Compare commits
No commits in common. "@solana/spl-token@v0.1.5" and "master" have entirely different histories.
@solana/sp
...
master
|
@ -10,34 +10,7 @@ on:
|
|||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
check_non_docs:
|
||||
outputs:
|
||||
run_all_github_action_checks: ${{ steps.check_files.outputs.run_all_github_action_checks }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: check modified files
|
||||
id: check_files
|
||||
run: |
|
||||
echo "========== check paths of modified files =========="
|
||||
echo "::set-output name=run_all_github_action_checks::true"
|
||||
git diff --name-only HEAD^ HEAD > files.txt
|
||||
while IFS= read -r file
|
||||
do
|
||||
if [[ $file != docs/** ]]; then
|
||||
echo "Found modified non-'docs' file(s)"
|
||||
echo "::set-output name=run_all_github_action_checks::false"
|
||||
break
|
||||
fi
|
||||
done < files.txt
|
||||
|
||||
all_github_action_checks:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check_non_docs
|
||||
if: needs.check_non_docs.outputs.run_all_github_action_checks == 'true'
|
||||
steps:
|
||||
- run: echo "Done"
|
||||
|
|
|
@ -8,5 +8,3 @@ node_modules
|
|||
hfuzz_target
|
||||
hfuzz_workspace
|
||||
**/*.so
|
||||
**/.DS_Store
|
||||
test-ledger
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,15 +9,14 @@ members = [
|
|||
"examples/rust/transfer-lamports",
|
||||
"feature-proposal/program",
|
||||
"feature-proposal/cli",
|
||||
"governance/program",
|
||||
"libraries/math",
|
||||
"memo/program",
|
||||
"name-service/program",
|
||||
"record/program",
|
||||
"shared-memory/program",
|
||||
"stake-pool/cli",
|
||||
"stake-pool/program",
|
||||
"token-lending/program",
|
||||
"token-lending/client",
|
||||
"token-swap/program",
|
||||
"token-swap/program/fuzz",
|
||||
"token/cli",
|
||||
|
@ -30,6 +29,3 @@ exclude = [
|
|||
"themis/program_ristretto",
|
||||
"token/perf-monitor", # TODO: Rework perf-monitor to use solana-program-test, avoiding the need to link directly with the BPF VM
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
|
|
@ -12,12 +12,12 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -13,7 +13,7 @@ test-bpf = []
|
|||
[dependencies]
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
spl-token = { version = "3.0", path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||
thiserror = "1.0"
|
||||
uint = "0.8"
|
||||
|
@ -21,8 +21,8 @@ arbitrary = { version = "0.4", features = ["derive"], optional = true }
|
|||
borsh = "0.8.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -4,10 +4,13 @@ set -ex
|
|||
cd "$(dirname "$0")/.."
|
||||
source ./ci/solana-version.sh install
|
||||
|
||||
(cd token/js && npm install)
|
||||
|
||||
cd token-swap/js
|
||||
npm install
|
||||
npm run lint
|
||||
npm run build
|
||||
npm run flow
|
||||
npx tsc module.d.ts
|
||||
npm run start-with-test-validator
|
||||
(cd ../../target/deploy && mv spl_token_swap_production.so spl_token_swap.so)
|
||||
SWAP_PROGRAM_OWNER_FEE_ADDRESS="HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN" npm run start-with-test-validator
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
if [[ -n $RUST_STABLE_VERSION ]]; then
|
||||
stable_version="$RUST_STABLE_VERSION"
|
||||
else
|
||||
stable_version=1.52.1
|
||||
stable_version=1.50.0
|
||||
fi
|
||||
|
||||
if [[ -n $RUST_NIGHTLY_VERSION ]]; then
|
||||
nightly_version="$RUST_NIGHTLY_VERSION"
|
||||
else
|
||||
nightly_version=2021-04-18
|
||||
nightly_version=2021-02-18
|
||||
fi
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
if [[ -n $SOLANA_VERSION ]]; then
|
||||
solana_version="$SOLANA_VERSION"
|
||||
else
|
||||
solana_version=v1.6.7
|
||||
solana_version=v1.5.15
|
||||
fi
|
||||
|
||||
export solana_version="$solana_version"
|
||||
|
|
|
@ -8,7 +8,6 @@ module.exports = {
|
|||
"token-lending",
|
||||
"associated-token-account",
|
||||
"memo",
|
||||
"name-service",
|
||||
"shared-memory",
|
||||
"stake-pool",
|
||||
"feature-proposal",
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
---
|
||||
title: Name Service
|
||||
---
|
||||
|
||||
A SPL program for issuing and managing ownership of: domain names, Solana Pubkeys, URLs, Twitter handles, ipfs cid's etc..
|
||||
|
||||
This program could be used for dns, pubkey etc lookups via a browser extension
|
||||
for example, the goal is to create an easy way to identify Solana public keys
|
||||
with various links.
|
||||
|
||||
Broader use cases are also imaginable.
|
||||
|
||||
Key points:
|
||||
- A Name is a string that maps to a record (program derived account) which can hold data.
|
||||
- Each name is of a certain class and has a certain owner, both are identified
|
||||
by their pubkeys. The class of a name needs to sign the issuance of it.
|
||||
- A name can have a parent name that is identified by the address of its record.
|
||||
The owner of the parent name (when it exists) needs to sign the issuance of
|
||||
the child name.
|
||||
- The data of a name registry is controlled by the class keypair or, when it is
|
||||
set to `Pubkey::default()`, by the name owner keypair.
|
||||
- Only the owner can delete a name registry.
|
||||
|
||||
|
||||
Remarks and use cases:
|
||||
- Domain name declarations: One could arbitrarily set-up a class that we can call
|
||||
Top-Level-Domain names. Names in this class can only be issued with the
|
||||
permission of the class keypair, ie the administrator, who can enforce that
|
||||
TLD names are of the type `".something"`. From then on one could create and
|
||||
own the TLD `".sol"` and create a class of ".sol" sub-domains, administrating
|
||||
the issuance of the `"something.sol"` sub-domains that way (by setting the
|
||||
parent name to the address of the `".sol"` registry).
|
||||
|
||||
An off-chain browser extension could then, similarly to DNS, parse the user SPL
|
||||
name service URL input and descend the chain of names, verifying that the names
|
||||
exist with the correct parenthood, and finally use the data of the last child
|
||||
name (or also a combination of the parents data) in order to resolve this call
|
||||
towards a real DNS URL or any kind of data.
|
||||
|
||||
Although the ownership and class system makes the administration a given class
|
||||
centralized, the creation of new classes is permissionless and as a class owner
|
||||
any kind of decentralized governance signing program could be used.
|
||||
|
||||
- Twitter handles can be added as names of one specific name class. The class
|
||||
authority of will therefore hold the right to add a Twitter handle name. This
|
||||
enables the verification of Twitter accounts for example by asking the user to
|
||||
tweet his pubkey or a signed message. A bot that holds the private issuing
|
||||
authority key can then sign the Create instruction (with a metadata_authority
|
||||
that is the tweeted pubkey) and send it back to the user who will then submit
|
||||
it to the program.
|
||||
In this case the class will still be able to control the data of the name registry, and not the user for example.
|
||||
|
||||
Therefore, another way of using this program would be to create a name
|
||||
(`"verified-twitter-handles"` for example) with the `Pubkey::default()` class
|
||||
and with the owner being the authority. That way verified Twitter names could be
|
||||
issued as child names of this parent by the owner, leaving the user as being
|
||||
able to modify the data of his Twitter name registry.
|
|
@ -3,7 +3,7 @@ title: Stake Pool Program
|
|||
---
|
||||
|
||||
A program for pooling together SOL to be staked by an off-chain agent running
|
||||
a Delegation Bot which redistributes the stakes across the network and tries
|
||||
a Delegation bot which redistributes the stakes across the network and tries
|
||||
to maximize censorship resistance and rewards.
|
||||
|
||||
## Overview
|
||||
|
@ -14,7 +14,7 @@ inflation rate, total number of SOL staked on the network, and an individual
|
|||
validator’s uptime and commission (fee).
|
||||
|
||||
Stake pools are an alternative method of earning staking rewards. This on-chain
|
||||
program pools together SOL to be staked by a staker, allowing SOL holders to
|
||||
program pools together SOL to be staked by a manager, allowing SOL holders to
|
||||
stake and earn rewards without managing stakes.
|
||||
|
||||
Additional information regarding staking and stake programming is available at:
|
||||
|
@ -24,18 +24,16 @@ Additional information regarding staking and stake programming is available at:
|
|||
|
||||
## Motivation
|
||||
|
||||
This document is intended for the main actors of the stake pool system:
|
||||
|
||||
* manager: creates and manages the stake pool, earns fees, can update the fee, staker, and manager
|
||||
* staker: adds and removes validators to the pool, rebalances stake among validators
|
||||
* user: provides staked SOL into an existing stake pool
|
||||
This document is intended for stake pool managers who want to create or manage
|
||||
stake pools, and users who want to provide staked SOL into an existing stake
|
||||
pool.
|
||||
|
||||
In its current iteration, the stake pool only processes totally active stakes.
|
||||
Deposits must come from fully active stakes, and withdrawals return a fully
|
||||
active stake account.
|
||||
|
||||
This means that stake pool managers, stakers, and users must be comfortable with
|
||||
creating and delegating stakes, which are more advanced operations than sending and
|
||||
This means that stake pool managers and users must be comfortable with creating
|
||||
and delegating stakes, which are more advanced operations than sending and
|
||||
receiving SPL tokens and SOL. Additional information on stake operations are
|
||||
available at:
|
||||
|
||||
|
@ -48,28 +46,27 @@ like [Token Swap](token-swap.md).
|
|||
|
||||
## Operation
|
||||
|
||||
A stake pool manager creates a stake pool, and the staker includes validators that will
|
||||
A stake pool manager creates a stake pool and includes validators that will
|
||||
receive delegations from the pool by creating "validator stake accounts" and
|
||||
activating a delegation on them. Once a validator stake account's delegation is
|
||||
active, the staker adds it to the stake pool.
|
||||
active, the stake pool manager adds it to the stake pool.
|
||||
|
||||
At this point, users can participate with deposits. They must delegate a stake
|
||||
account to the one of the validators in the stake pool. Once it's active, the
|
||||
user can deposit their stake into the pool in exchange for SPL staking derivatives
|
||||
representing their fractional ownership in pool. A percentage of the rewards
|
||||
earned by the pool goes to the pool manager as a fee.
|
||||
representing their fractional ownership in pool. A percentage of the user's
|
||||
deposit goes to the pool manager as a fee.
|
||||
|
||||
Over time, as the stakes in the stake pool accrue staking rewards, the user's fractional
|
||||
Over time, as the stake pool accrues staking rewards, the user's fractional
|
||||
ownership will be worth more than their initial deposit. Whenever the user chooses,
|
||||
they can withdraw their SPL staking derivatives in exchange for an activated stake.
|
||||
|
||||
The stake pool staker can add and remove validators, or rebalance the pool by
|
||||
decreasing the stake on a validator, waiting an epoch to move it into the stake
|
||||
pool's reserve account, then increasing the stake on another validator.
|
||||
The stake pool manager can add and remove validators, or rebalance the pool by
|
||||
withdrawing stakes from the pool, deactivating them, reactivating them on another
|
||||
validator, then depositing back into the pool.
|
||||
|
||||
The staker operation to add a new validator requires roughly 1.003 SOL to create
|
||||
the stake account on a validator, so the stake pool staker will need liquidity
|
||||
on hand to fully manage the pool stakes.
|
||||
These manager operations require SPL staking derivatives and staked SOL, so the
|
||||
stake pool manager will need liquidity on hand to properly manage the pool.
|
||||
|
||||
## Background
|
||||
|
||||
|
@ -133,105 +130,32 @@ Hardware Wallet URL (See [URL spec](https://docs.solana.com/wallet-guide/hardwar
|
|||
solana config set --keypair usb://ledger/
|
||||
```
|
||||
|
||||
#### Run Locally
|
||||
|
||||
If you would like to test a stake pool locally without having to wait for stakes
|
||||
to activate and deactivate, you can run the stake pool locally using the
|
||||
`solana-test-validator` tool with shorter epochs, and pulling the current program
|
||||
from devnet, testnet, or mainnet.
|
||||
|
||||
```sh
|
||||
$ solana-test-validator -c poo1B9L9nR3CrcaziKVYVpRX6A9Y1LAXYasjjfCbApj --url devnet --slots-per-epoch 32
|
||||
$ solana config set --url http://127.0.0.1:8899
|
||||
```
|
||||
|
||||
### Stake Pool Manager Examples
|
||||
### Stake Pool Administrator Examples
|
||||
|
||||
#### Create a stake pool
|
||||
|
||||
The stake pool manager controls the stake pool from a high level, and in exchange
|
||||
receives a fee in the form of SPL token staking derivatives. The manager
|
||||
sets the fee on creation. Let's create a pool with a 3% fee and a maximum of 1000
|
||||
validator stake accounts:
|
||||
The pool administrator manages the stake accounts in a stake pool, and in exchange
|
||||
receives a fee in the form of SPL token staking derivatives. The administrator
|
||||
sets the fee on creation. Let's create a pool with a 3% fee:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool create-pool --fee-numerator 3 --fee-denominator 100 --max-validators 1000
|
||||
Creating reserve stake 33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J
|
||||
Creating mint D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ
|
||||
Creating pool fee collection account 5gpuSdutGY98KKbgmR5CfLK7toFcQD69JzKDwseegzXE
|
||||
Signature: 2dvCtHMcqxibckhvVgFQeFCRb7VcHbuFLRf71Aqd9PtzFzdbG3gAkNpxYznfpKDx2vTRrVtwW81sZAx5U3Frb5Uu
|
||||
Creating stake pool EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
Signature: 2kYDVyJp8FVrLmEZyW9ivMYcXEsgWm4hFyhp5omxVtonjhYG6WS1S85sPTCdsQWe3idof6ZqsY8F3oaMXwrEkAYK
|
||||
$ spl-stake-pool create-pool --fee-numerator 3 --fee-denominator 100
|
||||
Creating mint Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS
|
||||
Creating pool fee collection account 3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5
|
||||
Creating stake pool 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
Signature: 5HdDoPssqwyLjt2QvhRbnSATZqFLGKha92zMuJiBUpKeKYKGURRV41N5ydCQxqnFjCud3xv85Z6ghErppNJzaYM8
|
||||
```
|
||||
|
||||
The unique stake pool identifier is `EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1`.
|
||||
The unique stake pool identifier is `3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC`.
|
||||
|
||||
The identifier for the SPL token for staking derivatives is
|
||||
`D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ`. The stake pool has full control
|
||||
`Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS`. The stake pool has full control
|
||||
over the mint.
|
||||
|
||||
The pool creator's fee account identifier is
|
||||
`5gpuSdutGY98KKbgmR5CfLK7toFcQD69JzKDwseegzXE`. Every epoch, as stake accounts
|
||||
in the stake pool earn rewards, the program will mint SPL token staking derivatives
|
||||
equal to 3% of the gains on that epoch into this account. If no gains were observed,
|
||||
nothing will be deposited.
|
||||
|
||||
The reserve stake account identifier is `33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J`.
|
||||
This account holds onto additional stake used when rebalancing between validators.
|
||||
|
||||
For a stake pool with 1000 validators, the cost to create a stake pool is less
|
||||
than 0.5 SOL.
|
||||
|
||||
#### Set manager
|
||||
|
||||
The stake pool manager may pass their administrator privileges to another account.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-manager EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --new-manager 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||
```
|
||||
|
||||
At the same time, they may also change the SPL token account that receives fees
|
||||
every epoch. The mint for the provided token account must be the SPL token mint,
|
||||
`D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ` in our example.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-manager EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --new-fee-receiver HoCsh97wRxRXVjtG7dyfsXSwH9VxdDzC7GvAsBE1eqJz
|
||||
Signature: 4aK8yzYvPBkP4PyuXTcCm529kjEH6tTt4ixc5D5ZyCrHwc4pvxAHj6wcr4cpAE1e3LddE87J1GLD466aiifcXoAY
|
||||
```
|
||||
|
||||
#### Set fee
|
||||
|
||||
The stake pool manager may update the fee assessed every epoch, passing the
|
||||
numerator and denominator for the fraction that make up the fee. For a fee of
|
||||
10%, they could run:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-fee EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 10 100
|
||||
Signature: 5yPXfVj5cbKBfZiEVi2UR5bXzVDuc2c3ruBwSjkAqpvxPHigwGHiS1mXQVE4qwok5moMWT5RNYAMvkE9bnfQ1i93
|
||||
```
|
||||
|
||||
#### Set staker
|
||||
|
||||
In order to manage the stake accounts, the stake pool manager or
|
||||
staker can set the staker authority of the stake pool's managed accounts.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-staker EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||
```
|
||||
|
||||
Now, the new staker can perform any normal stake pool operations, including
|
||||
adding and removing validators and rebalancing stake.
|
||||
|
||||
Important security note: the stake pool program only gives staking authority to
|
||||
the pool staker and always retains withdraw authority. Therefore, a malicious
|
||||
stake pool staker cannot steal funds from the stake pool.
|
||||
|
||||
Note: to avoid "disturbing the manager", the staker can also reassign their stake
|
||||
authority.
|
||||
|
||||
### Stake Pool Staker Examples
|
||||
`3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5`. When users deposit warmed up
|
||||
stake accounts into the stake pool, the program will transfer 3% of their
|
||||
contribution into this account in the form of SPL token staking derivatives.
|
||||
|
||||
#### Create a validator stake account
|
||||
|
||||
|
@ -246,7 +170,7 @@ lists, we choose some validators at random and start with identity
|
|||
delegated to that vote account.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
Creating stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Signature: 4pA2WKT6d2wkXEtSpiQswv22WyoFad2KX6FdPEzwBiEquvaUBEtzenys5Jh1ABPCh7yc4w8kzqMRRCwDj6ZSUV1K
|
||||
```
|
||||
|
@ -255,13 +179,13 @@ In order to maximize censorship resistance, we want to distribute our SOL to as
|
|||
many validators as possible, so let's add a few more.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz
|
||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz
|
||||
Creating stake account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie
|
||||
Signature: 4pyRZzjsWG7jP3GRZeZCo2Eb2TPjHM4kAYRFMivimme6HAee1nhzoNJBe3VSt2sv7acp5fwT7J8omBM8o3niY8gu
|
||||
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||
Creating stake account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
|
||||
Signature: 4ZUdZzUARgUCPuY8nVsJbN6vRDbVX8sYAQGYYXj2YVvjoJ2oevq2H8uzrhYApe419uoP7QYukqNstiti5p5DDukN
|
||||
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm
|
||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm
|
||||
Creating stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13
|
||||
Signature: yQqXCbuA66wQsHtkziNg3XadfZF5aCmvjfentwbZJnSPeEjJwPka3M1QY5GmR1efprptqaePn71BTMSLscX8DLr
|
||||
```
|
||||
|
@ -311,21 +235,22 @@ We created new validator stake accounts in the last step and staked them. Once
|
|||
the stake activates, we can add them to the stake pool.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool add-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
$ spl-stake-pool add-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Creating account to receive tokens Gu8xqzYFg2sPHWHhUivKNBeF9uikiauihLs9hLzziKu7
|
||||
Signature: 3N1K89rGV9gWueTTrPGTDBwKAp8BikQhKHMFoREw98Q1piXFeZSSxqfnRQexrfAZQfrpYH9qwsaPWRruwkVeBivV
|
||||
```
|
||||
|
||||
Users can start depositing their activated stakes into the stake pool, as
|
||||
long as they are delegated to the same vote account, which was
|
||||
`FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN` in this example. You can also
|
||||
`FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` in this example. You can also
|
||||
double-check that at any time using the Solana command-line utility.
|
||||
|
||||
```sh
|
||||
$ solana stake-account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Balance: 0.002282881 SOL
|
||||
Rent Exempt Reserve: 0.00228288 SOL
|
||||
Delegated Stake: 1.000000000 SOL
|
||||
Active Stake: 1.000000000 SOL
|
||||
Delegated Stake: 0.000000001 SOL
|
||||
Active Stake: 0.000000001 SOL
|
||||
Activating Stake: 0 SOL
|
||||
Stake activates starting from epoch: 161
|
||||
Delegated Vote Account Address: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
|
@ -335,31 +260,26 @@ Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
|||
|
||||
#### Remove validator stake account
|
||||
|
||||
If the stake pool staker wants to stop delegating to a vote account, they can
|
||||
totally remove the validator stake account from the stake pool.
|
||||
If the stake pool manager wants to stop delegating to a vote account, they can
|
||||
totally remove the validator stake account from the stake pool by providing
|
||||
staking derivatives, just like `withdraw`.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool remove-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||
$ spl-stake-pool remove-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d
|
||||
```
|
||||
|
||||
The difference with `withdraw` is that the validator stake account is totally
|
||||
removed from the stake pool and now belongs to the administrator. The authority
|
||||
for the withdrawn stake account can also be specified using the `--new-authority` flag:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool remove-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G --new-authority 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d
|
||||
```
|
||||
removed from the stake pool and now belongs to the administrator.
|
||||
|
||||
We can check the removed stake account:
|
||||
|
||||
```sh
|
||||
$ solana stake-account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
|
||||
Balance: 1.002282880 SOL
|
||||
Balance: 1.002282881 SOL
|
||||
Rent Exempt Reserve: 0.00228288 SOL
|
||||
Delegated Stake: 1.000000000 SOL
|
||||
Active Stake: 1.000000000 SOL
|
||||
Delegated Stake: 1.000000001 SOL
|
||||
Active Stake: 1.000000001 SOL
|
||||
Delegated Vote Account Address: AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||
Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
|
@ -371,7 +291,7 @@ removal of staked SOL from the pool.
|
|||
We can also double-check that the stake pool no longer shows the stake account:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
|
||||
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
|
||||
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
|
||||
|
@ -380,14 +300,14 @@ Total: ◎15.849959206
|
|||
|
||||
#### Rebalance the stake pool
|
||||
|
||||
As time goes on, users will deposit to and withdraw from all of the stake accounts
|
||||
managed by the pool, and the stake pool staker may want to rebalance the stakes.
|
||||
As time goes on, deposits and withdrawals will happen to all of the stake accounts
|
||||
managed by the pool, and the stake pool manager may want to rebalance the stakes.
|
||||
|
||||
For example, let's say the staker wants the same delegation to every validator
|
||||
For example, let's say the manager wants the same delegation to every validator
|
||||
in the pool. When they look at the state of the pool, they see:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
|
||||
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
|
||||
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
|
||||
|
@ -395,63 +315,75 @@ Total: ◎15.849959206
|
|||
```
|
||||
|
||||
This isn't great! The last stake account, `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`
|
||||
has too much allocated. For their strategy, the staker wants the `15.849959206`
|
||||
has too much allocated. For their strategy, the manager wants the `15.849959206`
|
||||
SOL to be distributed evenly, meaning around `5.283319735` in each account. They need
|
||||
to move `4.281036854` to `FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` and
|
||||
`1.872447062` to `FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN`.
|
||||
|
||||
##### Decrease validator stake
|
||||
|
||||
First, they need to decrease the amount on stake account
|
||||
`E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`, delegated to
|
||||
`HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz`, by total of `6.153483916` SOL.
|
||||
|
||||
They decrease that amount of SOL:
|
||||
First, they need to withdraw a total of `6.153483916` from
|
||||
`E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`. Using the `spl-token` utility,
|
||||
let's check the total supply of pool tokens:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool decrease-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz 6.153483916
|
||||
Signature: ZpQGwT85rJ8Y9afdkXhKo3TVv4xgTz741mmZj2vW7mihYseAkFsazWxza2y8eNGY4HDJm15c1cStwyiQzaM3RpH
|
||||
$ spl-token supply Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS
|
||||
0.034692168
|
||||
```
|
||||
|
||||
Internally, this instruction splits and deactivates 6.153483916 SOL from the
|
||||
validator stake account `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie` into a
|
||||
transient stake account, owned and managed entirely by the stake pool.
|
||||
Given a total pool token supply of `0.034692168` and total staked SOL amount of
|
||||
`15.849959206`, let's calculate how many pool tokens to withdraw from the pool:
|
||||
|
||||
Once the stake is deactivated during the next epoch, the `update` command will
|
||||
automatically merge the transient stake account into a reserve stake account,
|
||||
also entirely owned and managed by the stake pool.
|
||||
|
||||
##### Increase validator stake
|
||||
|
||||
Now that the reserve stake account has enough to perform the rebalance, the staker
|
||||
can increase the stake on the two other validators,
|
||||
`8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm` and
|
||||
`2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`.
|
||||
|
||||
They add 4.281036854 SOL to `8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm`:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm 4.281036854
|
||||
Signature: 3GJACzjUGLPjcd9RLUW86AfBLWKapZRkxnEMc2yHT6erYtcKBgCapzyrVH6VN8Utxj7e2mtvzcigwLm6ZafXyTMw
|
||||
```
|
||||
sol_to_withdraw * total_pool_tokens / total_sol_staked = pool_tokens_to_withdraw
|
||||
6.153483916 * 0.034692168 / 15.849959206 ~ 0.013468659
|
||||
```
|
||||
|
||||
And they add 1.872447062 SOL to `2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`:
|
||||
They withdraw that amount of pool tokens:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 1.872447062
|
||||
Signature: 4zaKYu3MQ3as8reLbuHKaXN8FNaHvpHuiZtsJeARo67UKMo6wUUoWE88Fy8N4EYQYicuwULTNffcUD3a9jY88PoU
|
||||
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.013468659 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Withdrawing from account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie, amount ◎6.153483855, 0.013468659 pool tokens
|
||||
Creating account to receive stake 8ykyY7maA9HUfUphZHBkhsnydY5gFfyHFSfxCA7imqrk
|
||||
Signature: z8a5ZRfWdj8Fcsr3ttCJ731wFKyhZNcqoKEdV1RBCkzr3tHGQNCC56qvRVJ6oxyCVDqWZ3KL1Bkyn3sDpjYPDku
|
||||
```
|
||||
|
||||
Internally, this instruction also uses transient stake accounts. This time, the
|
||||
stake pool splits from the reserve stake, into the transient stake account,
|
||||
then activates it to the appropriate validator.
|
||||
Because of rounding in the calculation a few lines above, it looks like we receive
|
||||
less than we should. If we play that back the other way, we'll see that all is well:
|
||||
|
||||
One to two epochs later, once the transient stakes activate, the `update` command
|
||||
automatically merges the transient stakes into the validator stake account, leaving
|
||||
a fully rebalanced stake pool:
|
||||
```
|
||||
pool_tokens_to_withdraw * total_sol_staked / total_pool_tokens = sol_to_withdraw
|
||||
0.013468659 * 15.849959206 / 0.034692168 ~ 6.153483855
|
||||
```
|
||||
|
||||
Next, they deactivate the new received stake:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
$ solana deactivate-stake 8ykyY7maA9HUfUphZHBkhsnydY5gFfyHFSfxCA7imqrk
|
||||
Signature: 4SuwZK5JvYkYVkM5yfu2x8x6iou6558teMwzphGECLmstMVoWbSvngUH48Ra24PrxtgUDyVDA8SXYS1qMyx3fjMj
|
||||
```
|
||||
|
||||
Once the stake is deactivated during the next epoch, they split the stake
|
||||
and activate it on the other two validator vote accounts. For brevity, those
|
||||
commands are omitted.
|
||||
|
||||
Eventually, we are left with stake account `4zppED2kFodUS2hBf8Fzeepu6yZ6QuyeNPBXCT9VU6fK`
|
||||
with `4.281036854` delegated to `8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm`
|
||||
and stake account `GCJnuFGCDzaToPwJtG5GiK4g3DJBfuhQy6388NyGcfwf` with `1.872447062`
|
||||
delegated to `2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`.
|
||||
|
||||
Once the new stakes are ready, the manager deposits them back into the stake pool:
|
||||
```sh
|
||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC GCJnuFGCDzaToPwJtG5GiK4g3DJBfuhQy6388NyGcfwf --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Signature: jKsdEr3zxF2zZs78rmrP3PmQiTwE7v15ieEuxp4db1VQe9owXVGM8nM3dJqVRHXPsS4frQW4gJ6xBfTTk2HvKDX
|
||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4zppED2kFodUS2hBf8Fzeepu6yZ6QuyeNPBXCT9VU6fK --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Depositing into stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13
|
||||
Signature: 3JXvTvea6F4Epd2krSxnTRZPB4gLZ8GqisFE58Z4ocV92fDN1HRMVPoPhJtYcfuF12vyQZUueKwVmkvL6Wgf2evc
|
||||
```
|
||||
|
||||
Leaving them with a rebalanced stake pool!
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎5.283340235
|
||||
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎5.283612231
|
||||
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎5.284317422
|
||||
|
@ -459,7 +391,33 @@ Total: ◎15.851269888
|
|||
```
|
||||
|
||||
Due to staking rewards that accrued during the rebalancing process, the pool is
|
||||
not perfectly balanced. This is completely normal.
|
||||
not prefectly balanced. This is completely normal.
|
||||
|
||||
#### Set staking authority
|
||||
|
||||
In order to manage the stake accounts more directly, the stake pool owner can
|
||||
set the stake authority of the stake pool's managed accounts.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-staking-auth 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --stake-account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN --new-staker 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||
```
|
||||
|
||||
Now, the new staking authority can perform any normal staking operations,
|
||||
including deactivating or re-staking.
|
||||
|
||||
Important security note: the stake pool program only gives staking authority to
|
||||
the pool owner and always retains withdraw authority. Therefore, a malicious
|
||||
stake pool manager cannot steal funds from the stake pool.
|
||||
|
||||
#### Set owner
|
||||
|
||||
The stake pool owner may pass their administrator privileges to another account.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --new-owner 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||
```
|
||||
|
||||
### User Examples
|
||||
|
||||
|
@ -471,7 +429,7 @@ command-line utility has a special instruction for finding out which vote
|
|||
accounts are already associated with the stake pool.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E 1.002282880 SOL
|
||||
E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie 1.002282880 SOL
|
||||
FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN 1.002282880 SOL
|
||||
|
@ -483,13 +441,13 @@ If the manager has recently created the stake pool, and there are no stake
|
|||
accounts present yet, the command-line utility will inform us.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
No accounts found.
|
||||
```
|
||||
|
||||
#### Deposit stake
|
||||
|
||||
Stake pools only accept deposits from active accounts, so we must first
|
||||
Stake pools only accept deposits from fully staked accounts, so we must first
|
||||
create stake accounts and delegate them to one of the validators managed by the
|
||||
stake pool. Using the `list` command from the previous section, we see that
|
||||
`2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3` is a valid vote account, so let's
|
||||
|
@ -515,19 +473,17 @@ Two epochs later, when the stake is fully active and has received one epoch of
|
|||
rewards, we can deposit the stake into the stake pool.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool deposit EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa
|
||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa
|
||||
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Creating account to receive tokens 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
|
||||
```
|
||||
|
||||
The CLI will default to using the fee payer's
|
||||
[Associated Token Account](associated-token-account.md) for stake pool tokens.
|
||||
Alternatively, you can create an SPL token account yourself and pass it as the
|
||||
`token-receiver` for the command.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool deposit EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
|
||||
```
|
||||
|
@ -549,8 +505,7 @@ In order to calculate the proper value of these stake pool tokens, we must updat
|
|||
the total value managed by the stake pool every epoch.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
Updating stake pool...
|
||||
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
||||
```
|
||||
|
||||
|
@ -558,33 +513,13 @@ If another user already updated the stake pool balance for the current epoch, we
|
|||
see a different output.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
Update not required
|
||||
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
Stake pool balances are up to date, no update required.
|
||||
```
|
||||
|
||||
If no one updates the stake pool in the current epoch, the deposit and withdraw
|
||||
instructions will fail. The update instruction is permissionless, so any user
|
||||
can run it before depositing or withdrawing. As a convenience, the CLI attempts
|
||||
to update before running any instruction on the stake pool.
|
||||
|
||||
If the stake pool transient stakes are in an unexpected state, and merges are
|
||||
not possible, there is the option to only update the stake pool balances without
|
||||
performing merges using the `--no-merge` flag.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --no-merge
|
||||
Updating stake pool...
|
||||
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
||||
```
|
||||
|
||||
Later on, whenever the transient stakes are ready to be merged, it is possible to
|
||||
force another update in the same epoch using the `--force` flag.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --force
|
||||
Updating stake pool...
|
||||
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
||||
```
|
||||
can run it before depositing or withdrawing.
|
||||
|
||||
#### Withdraw stake
|
||||
|
||||
|
@ -594,7 +529,7 @@ staking derivative SPL tokens in exchange for an activated stake account.
|
|||
Let's withdraw 0.02 staking derivative tokens from the stake pool.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02
|
||||
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
||||
|
@ -615,58 +550,15 @@ Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
|||
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
```
|
||||
|
||||
Alternatively, the user can specify an existing uninitialized stake account to
|
||||
receive their stake using the `--stake-receiver` parameter.
|
||||
Alternatively, the user can specify an existing stake account to receive their
|
||||
stake using the `stake-receiver` parameter.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF --stake-receiver CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF --stake-receiver CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
||||
```
|
||||
|
||||
By default, the withdraw command uses the fee payer's associated token account to
|
||||
source the derivative tokens. It's possible to specify the SPL token account using
|
||||
the `--pool-account` flag.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02 --pool-account 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
||||
```
|
||||
|
||||
By default, the withdraw command will withdraw from the largest validator stake
|
||||
accounts in the pool. It's also possible to specify a specific vote account for
|
||||
the withdraw using the `--vote-account` flag.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02 --vote-account 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
||||
```
|
||||
|
||||
Note that the associated validator stake account must have enough lamports to
|
||||
satisfy the pool token amount requested.
|
||||
|
||||
##### Special case: exiting pool with a delinquent staker
|
||||
|
||||
With the reserve stake, it's possible for a delinquent or malicious staker to
|
||||
move all stake into the reserve through `decrease-validator-stake`, so the
|
||||
staking derivatives will not gain rewards, and the stake pool users will not
|
||||
be able to withdraw their funds.
|
||||
|
||||
To get around this case, it is also possible to withdraw from the stake pool's
|
||||
reserve, but only if all of the validator stake accounts are at the minimum amount of
|
||||
`1 SOL + stake account rent exemption`.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02 --use-reserve
|
||||
Withdrawing from account 33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Creating account to receive stake 9E5YzXXu9NDhtMxWJKCwe2M8Sdz6vL6bcBS92U76PVtE
|
||||
Signature: 4aZaeT9Azcq23PdKcjbQLseNveZVAQ4xMabBGQspfX316cE62Q2hoES373ExbT9y2JUhug7SgdybNaCjuZ6uqNYf
|
||||
```
|
||||
|
||||
## Appendix
|
||||
|
||||
### Activated stakes
|
||||
|
@ -677,22 +569,6 @@ are not equivalent to inactive, activating, or deactivating stakes due to the
|
|||
time cost of staking. Otherwise, malicious actors can deposit stake in one state
|
||||
and withdraw it in another state without waiting.
|
||||
|
||||
### Transient stake accounts
|
||||
|
||||
Each validator gets one transient stake account, so the staker can only
|
||||
perform one action at a time on a validator. It's impossible to increase
|
||||
and decrease the stake on a validator at the same time. The staker must wait for
|
||||
the existing transient stake account to get merged during an `update` instruction
|
||||
before performing a new action.
|
||||
|
||||
### Reserve stake account
|
||||
|
||||
Every stake pool is initialized with an undelegated reserve stake account, used
|
||||
to hold undelegated stake in process of rebalancing. After the staker decreases
|
||||
the stake on a validator, one epoch later, the update operation will merge the
|
||||
decreased stake into the reserve. Conversely, whenever the staker increases the
|
||||
stake on a validator, the lamports are drawn from the reserve stake account.
|
||||
|
||||
### Staking Credits Observed on Deposit
|
||||
|
||||
A deposited stake account's "credits observed" must match the destination
|
||||
|
|
|
@ -83,15 +83,6 @@ Hardware Wallet URL (See [URL spec](https://docs.solana.com/wallet-guide/hardwar
|
|||
solana config set --keypair usb://ledger/
|
||||
```
|
||||
|
||||
#### Airdrop SOL
|
||||
|
||||
Creating tokens and accounts requires SOL for account rent deposits and
|
||||
transaction fees. If the cluster you are targeting offers a faucet, you can get
|
||||
a little SOL for testing:
|
||||
```
|
||||
solana airdrop 1
|
||||
```
|
||||
|
||||
### Example: Creating your own fungible token
|
||||
|
||||
```sh
|
||||
|
@ -118,7 +109,7 @@ Signature: 42Sa5eK9dMEQyvD9GMHuKxXf55WLZ7tfjabUKDhNoZRAxj9MsnN7omriWMEHXLea3aYpj
|
|||
|
||||
`7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi` is now an empty account:
|
||||
```sh
|
||||
$ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||
0
|
||||
```
|
||||
|
||||
|
@ -135,7 +126,7 @@ The token `supply` and account `balance` now reflect the result of minting:
|
|||
```sh
|
||||
$ spl-token supply AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||
100
|
||||
$ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||
100
|
||||
```
|
||||
|
||||
|
@ -175,7 +166,7 @@ address by running `solana address` and provides it to the sender.
|
|||
|
||||
The sender then runs:
|
||||
```
|
||||
$ spl-token transfer AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
$ spl-token transfer 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
Transfer 50 tokens
|
||||
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
|
@ -193,7 +184,7 @@ The receiver obtains their wallet address by running `solana address` and provid
|
|||
The sender then runs to fund the receiver's associated token account, at the
|
||||
sender's expense, and then transfers 50 tokens into it:
|
||||
```
|
||||
$ spl-token transfer --fund-recipient AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
$ spl-token transfer --fund-recipient 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
Transfer 50 tokens
|
||||
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
|
@ -237,9 +228,9 @@ CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe
|
|||
|
||||
### Example: Create a non-fungible token
|
||||
|
||||
Create the token type with nine decimal places,
|
||||
Create the token type,
|
||||
```
|
||||
$ spl-token create-token --decimals 9
|
||||
$ spl-token create-token
|
||||
Creating token 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
|
||||
Signature: 4kz82JUey1B9ki1McPW7NYv1NqPKCod6WNptSkYqtuiEsQb9exHaktSAHJJsm4YxuGNW4NugPJMFX9ee6WA2dXts
|
||||
```
|
||||
|
@ -273,7 +264,7 @@ Now the `7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM` account holds the
|
|||
one and only `559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z` token:
|
||||
|
||||
```
|
||||
$ spl-token account-info 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
|
||||
$ spl-token account-info 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM
|
||||
|
||||
Address: 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM
|
||||
Balance: 1
|
||||
|
@ -529,7 +520,7 @@ There is a rich set of JSON RPC methods available for use with SPL Token:
|
|||
|
||||
See https://docs.solana.com/apps/jsonrpc-api for more details.
|
||||
|
||||
Additionally the versatile `getProgramAccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.
|
||||
Additionally the versatile `getProgramAcccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.
|
||||
|
||||
### Finding all token accounts for a specific mint
|
||||
|
||||
|
@ -562,7 +553,7 @@ curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/js
|
|||
```
|
||||
|
||||
The `"dataSize": 165` filter selects all [Token
|
||||
Account](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
|
||||
Acccount](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
|
||||
and then the `"memcmp": ...` filter selects based on the
|
||||
[mint](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L88)
|
||||
address within each token account.
|
||||
|
@ -597,7 +588,7 @@ curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/js
|
|||
```
|
||||
|
||||
The `"dataSize": 165` filter selects all [Token
|
||||
Account](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
|
||||
Acccount](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
|
||||
and then the `"memcmp": ...` filter selects based on the
|
||||
[owner](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L90)
|
||||
address within each token account.
|
||||
|
@ -861,13 +852,3 @@ the maximum allowed transaction size, remove those extra clean up instructions.
|
|||
They can be cleaned up during the next send operation.
|
||||
|
||||
The `spl-token gc` command provides an example implementation of this cleanup process.
|
||||
|
||||
|
||||
### Token Vesting Contract:
|
||||
This program allows you to lock arbitrary SPL tokens and release the locked tokens with a determined unlock schedule. An `unlock schedule` is made of a `unix timestamp` and a token `amount`, when initializing a vesting contract, the creator can pass an array of `unlock schedule` with an arbitrary size giving the creator of the contract complete control of how the tokens unlock over time.
|
||||
|
||||
Unlocking works by pushing a permissionless crank on the contract that moves the tokens to the pre-specified address. The recipient address of a vesting contract can be modified by the owner of the current recipient key, meaning that vesting contract locked tokens can be traded.
|
||||
|
||||
- Code: [https://github.com/Bonfida/token-vesting](https://github.com/Bonfida/token-vesting)
|
||||
- UI: [https://vesting.bonfida.com/#/](https://vesting.bonfida.com/#/)
|
||||
- Audit: The audit was conducted by Kudelski, the report can be found [here](https://github.com/Bonfida/token-vesting/blob/master/audit/Bonfida_SecurityAssessment_Vesting_Final050521.pdf)
|
|
@ -22,7 +22,7 @@ extern uint64_t do_invoke(SolParameters *params) {
|
|||
const SolSignerSeeds signers_seeds[] = {{seeds, SOL_ARRAY_SIZE(seeds)}};
|
||||
|
||||
SolPubkey expected_allocated_key;
|
||||
if (SUCCESS != sol_create_program_address(seeds, SOL_ARRAY_SIZE(seeds),
|
||||
if (SUCCESS == sol_create_program_address(seeds, SOL_ARRAY_SIZE(seeds),
|
||||
params->program_id,
|
||||
&expected_allocated_key)) {
|
||||
return ERROR_INVALID_INSTRUCTION_DATA;
|
||||
|
@ -31,7 +31,8 @@ extern uint64_t do_invoke(SolParameters *params) {
|
|||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SolAccountMeta arguments[] = {{allocated_info->key, true, true}};
|
||||
SolAccountMeta arguments[] = {{system_program_info->key, false, false},
|
||||
{allocated_info->key, true, true}};
|
||||
uint8_t data[4 + 8]; // Enough room for the Allocate instruction
|
||||
*(uint16_t *)data = 8; // Allocate instruction enum value
|
||||
*(uint64_t *)(data + 4) = SIZE; // Size to allocate
|
||||
|
|
|
@ -13,11 +13,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -15,11 +15,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -13,11 +13,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -13,11 +13,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -17,28 +17,24 @@ pub fn process_instruction(
|
|||
// Create in iterator to safety reference accounts in the slice
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
// Get the clock sysvar via syscall
|
||||
let clock_via_sysvar = Clock::get()?;
|
||||
// Or deserialize the account into a clock struct
|
||||
// The first account is the clock sysvar
|
||||
let clock_sysvar_info = next_account_info(account_info_iter)?;
|
||||
let clock_via_account = Clock::from_account_info(&clock_sysvar_info)?;
|
||||
// Both produce the same sysvar
|
||||
assert_eq!(clock_via_sysvar, clock_via_account);
|
||||
// Note: `format!` can be very expensive, use cautiously
|
||||
msg!("{:?}", clock_via_sysvar);
|
||||
|
||||
// Get the rent sysvar via syscall
|
||||
let rent_via_sysvar = Rent::get()?;
|
||||
// Or deserialize the account into a rent struct
|
||||
// The second account is the rent sysvar
|
||||
let rent_sysvar_info = next_account_info(account_info_iter)?;
|
||||
let rent_via_account = Rent::from_account_info(&rent_sysvar_info)?;
|
||||
// Both produce the same sysvar
|
||||
assert_eq!(rent_via_sysvar, rent_via_account);
|
||||
|
||||
// Deserialize the account into a clock struct
|
||||
let clock = Clock::from_account_info(&clock_sysvar_info)?;
|
||||
|
||||
// Deserialize the account into a rent struct
|
||||
let rent = Rent::from_account_info(&rent_sysvar_info)?;
|
||||
|
||||
// Note: `format!` can be very expensive, use cautiously
|
||||
msg!("{:?}", clock);
|
||||
// Can't print `exemption_threshold` because BPF does not support printing floats
|
||||
msg!(
|
||||
"Rent: lamports_per_byte_year: {:?}, burn_percent: {:?}",
|
||||
rent_via_sysvar.lamports_per_byte_year,
|
||||
rent_via_sysvar.burn_percent
|
||||
rent.lamports_per_byte_year,
|
||||
rent.burn_percent
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -12,11 +12,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -10,11 +10,11 @@ edition = "2018"
|
|||
[dependencies]
|
||||
chrono = "0.4.19"
|
||||
clap = "2.33.3"
|
||||
solana-clap-utils = "1.6.7"
|
||||
solana-cli-config = "1.6.7"
|
||||
solana-client = "1.6.7"
|
||||
solana-logger = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-clap-utils = "1.6.2"
|
||||
solana-cli-config = "1.6.2"
|
||||
solana-client = "1.6.2"
|
||||
solana-logger = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
spl-feature-proposal = { version = "1.0", path = "../program", features = ["no-entrypoint"] }
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -12,14 +12,15 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
borsh = "0.8"
|
||||
borsh = "0.7.1"
|
||||
borsh-derive = "0.8.1"
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
futures = "0.3"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -155,12 +155,13 @@ pub fn tally(feature_proposal_address: &Pubkey) -> Instruction {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::borsh_utils;
|
||||
|
||||
#[test]
|
||||
fn test_get_packed_len() {
|
||||
assert_eq!(
|
||||
FeatureProposalInstruction::get_packed_len(),
|
||||
solana_program::borsh::get_packed_len::<FeatureProposalInstruction>()
|
||||
borsh_utils::get_packed_len::<FeatureProposalInstruction>()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#![deny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod borsh_utils;
|
||||
mod entrypoint;
|
||||
pub mod instruction;
|
||||
pub mod processor;
|
||||
|
|
|
@ -59,12 +59,13 @@ impl Pack for FeatureProposal {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::borsh_utils;
|
||||
|
||||
#[test]
|
||||
fn test_get_packed_len() {
|
||||
assert_eq!(
|
||||
FeatureProposal::get_packed_len(),
|
||||
solana_program::borsh::get_packed_len::<FeatureProposal>()
|
||||
borsh_utils::get_packed_len::<FeatureProposal>()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
// Mark this test as BPF-only due to current `ProgramTest` limitations when CPIing into the system program
|
||||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use {
|
||||
solana_program::{
|
||||
use futures::{Future, FutureExt};
|
||||
use solana_program::{
|
||||
feature::{self, Feature},
|
||||
program_option::COption,
|
||||
program_pack::Pack,
|
||||
pubkey::Pubkey,
|
||||
system_program,
|
||||
},
|
||||
solana_program_test::*,
|
||||
solana_sdk::{
|
||||
};
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
signature::{Keypair, Signer},
|
||||
transaction::Transaction,
|
||||
},
|
||||
spl_feature_proposal::{instruction::*, state::*, *},
|
||||
};
|
||||
use spl_feature_proposal::{instruction::*, state::*, *};
|
||||
use std::io;
|
||||
|
||||
fn program_test() -> ProgramTest {
|
||||
ProgramTest::new(
|
||||
|
@ -24,6 +25,21 @@ fn program_test() -> ProgramTest {
|
|||
)
|
||||
}
|
||||
|
||||
/// Fetch and unpack account data
|
||||
fn get_account_data<T: Pack>(
|
||||
banks_client: &mut BanksClient,
|
||||
address: Pubkey,
|
||||
) -> impl Future<Output = std::io::Result<T>> + '_ {
|
||||
banks_client.get_account(address).map(|result| {
|
||||
let account =
|
||||
result?.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "account not found"))?;
|
||||
|
||||
T::unpack_from_slice(&account.data)
|
||||
.ok()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to deserialize account"))
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_basic() {
|
||||
let feature_proposal = Keypair::new();
|
||||
|
@ -52,17 +68,16 @@ async fn test_basic() {
|
|||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
// Confirm feature id account is now funded and allocated, but not assigned
|
||||
let feature_id_account = banks_client
|
||||
let feature_id_acccount = banks_client
|
||||
.get_account(feature_id_address)
|
||||
.await
|
||||
.expect("success")
|
||||
.expect("some account");
|
||||
assert_eq!(feature_id_account.owner, system_program::id());
|
||||
assert_eq!(feature_id_account.data.len(), Feature::size_of());
|
||||
assert_eq!(feature_id_acccount.owner, system_program::id());
|
||||
assert_eq!(feature_id_acccount.data.len(), Feature::size_of());
|
||||
|
||||
// Confirm mint account state
|
||||
let mint = banks_client
|
||||
.get_packed_account_data::<spl_token::state::Mint>(mint_address)
|
||||
let mint = get_account_data::<spl_token::state::Mint>(&mut banks_client, mint_address)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(mint.supply, 42);
|
||||
|
@ -71,8 +86,8 @@ async fn test_basic() {
|
|||
assert_eq!(mint.mint_authority, COption::Some(mint_address));
|
||||
|
||||
// Confirm distributor token account state
|
||||
let distributor_token = banks_client
|
||||
.get_packed_account_data::<spl_token::state::Account>(distributor_token_address)
|
||||
let distributor_token =
|
||||
get_account_data::<spl_token::state::Account>(&mut banks_client, distributor_token_address)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(distributor_token.amount, 42);
|
||||
|
@ -81,8 +96,8 @@ async fn test_basic() {
|
|||
assert!(distributor_token.close_authority.is_none());
|
||||
|
||||
// Confirm acceptance token account state
|
||||
let acceptance_token = banks_client
|
||||
.get_packed_account_data::<spl_token::state::Account>(acceptance_token_address)
|
||||
let acceptance_token =
|
||||
get_account_data::<spl_token::state::Account>(&mut banks_client, acceptance_token_address)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(acceptance_token.amount, 0);
|
||||
|
@ -100,17 +115,15 @@ async fn test_basic() {
|
|||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
// Confirm feature id account is not yet assigned
|
||||
let feature_id_account = banks_client
|
||||
let feature_id_acccount = banks_client
|
||||
.get_account(feature_id_address)
|
||||
.await
|
||||
.expect("success")
|
||||
.expect("some account");
|
||||
assert_eq!(feature_id_account.owner, system_program::id());
|
||||
assert_eq!(feature_id_acccount.owner, system_program::id());
|
||||
|
||||
assert!(matches!(
|
||||
banks_client
|
||||
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
|
||||
.await,
|
||||
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
|
||||
Ok(FeatureProposal::Pending(_))
|
||||
));
|
||||
|
||||
|
@ -145,18 +158,16 @@ async fn test_basic() {
|
|||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
// Confirm feature id account is now assigned
|
||||
let feature_id_account = banks_client
|
||||
let feature_id_acccount = banks_client
|
||||
.get_account(feature_id_address)
|
||||
.await
|
||||
.expect("success")
|
||||
.expect("some account");
|
||||
assert_eq!(feature_id_account.owner, feature::id());
|
||||
assert_eq!(feature_id_acccount.owner, feature::id());
|
||||
|
||||
// Confirm feature proposal account state
|
||||
assert!(matches!(
|
||||
banks_client
|
||||
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
|
||||
.await,
|
||||
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
|
||||
Ok(FeatureProposal::Accepted {
|
||||
tokens_upon_acceptance: 42
|
||||
})
|
||||
|
@ -186,9 +197,7 @@ async fn test_expired() {
|
|||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
banks_client
|
||||
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
|
||||
.await,
|
||||
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
|
||||
Ok(FeatureProposal::Pending(_))
|
||||
));
|
||||
|
||||
|
@ -199,9 +208,7 @@ async fn test_expired() {
|
|||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
banks_client
|
||||
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
|
||||
.await,
|
||||
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
|
||||
Ok(FeatureProposal::Expired)
|
||||
));
|
||||
}
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
# Governance
|
||||
|
||||
Governance is a program the chief purpose of which is to control the upgrade of other programs through democratic means.
|
||||
It can also be used as an authority provider for mints and other forms of access control as well where we may want
|
||||
a voting population to vote on disbursement of access or funds collectively.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Accounts diagram
|
||||
|
||||

|
||||
|
||||
### Governance Realm account
|
||||
|
||||
Governance Realm ties Community Token Mint and optional Council Token mint to create a realm
|
||||
for any governance pertaining to the community of the token holders.
|
||||
For example a trading protocol can issue a governance token and use it to create its governance realm.
|
||||
|
||||
Once a realm is created voters can deposit Governing tokens (Community or Council) to the realm and
|
||||
use the deposited amount as their voting weight to vote on Proposals within that realm.
|
||||
|
||||
### Program Governance account
|
||||
|
||||
The basic building block of governance to update programs is the ProgramGovernance account.
|
||||
It ties a governed Program ID and holds configuration options defining governance rules.
|
||||
The governed Program ID is used as the seed for a [Program Derived Address](https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses),
|
||||
and this program derived address is what is used as the address of the Governance account for your Program ID
|
||||
and the corresponding Governance mint and Council mint (if provided).
|
||||
|
||||
What this means is that there can only ever be ONE Governance account for a given Program.
|
||||
The governance program validates at creation time of the Governance account that the current upgrade authority of the program
|
||||
taken under governance signed the transaction.
|
||||
|
||||
Note: In future versions, once allowed in solana runtime, the governance program will take over the upgrade authority
|
||||
of the governed program when the Governance account is created.
|
||||
|
||||
### How does authority work?
|
||||
|
||||
Governance can handle arbitrary executions of code, but it's real power lies in the power to upgrade programs.
|
||||
It does this through executing commands to the bpf-upgradable-loader program.
|
||||
Bpf-upgradable-loader allows any signer who has Upgrade authority over a Buffer account and the Program account itself
|
||||
to upgrade it using its Upgrade command.
|
||||
Normally, this is the developer who created and deployed the program, and this creation of the Buffer account containing
|
||||
the new program data and overwriting of the existing Program account's data with it is handled in the background for you
|
||||
by the Solana program deploy cli command.
|
||||
However, in order for Governance to be useful, Governance now needs this authority.
|
||||
|
||||
### Proposal accounts
|
||||
|
||||
A Proposal is an instance of a Governance created to vote on and execute given set of changes.
|
||||
It is created by someone (Proposal Admin) and tied to a given Governance account
|
||||
and has a set of executable commands to it, a name and a description.
|
||||
It goes through various states (draft, voting, executing) and users can vote on it
|
||||
if they have relevant Community or Council tokens.
|
||||
It's rules are determined by the Governance account that it is tied to, and when it executes,
|
||||
it is only eligible to use the [Program Derived Address](https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses)
|
||||
authority given by the Governance account.
|
||||
So a Proposal for Sushi cannot for instance upgrade the Program for Uniswap.
|
||||
|
||||
When a Proposal is created by a user then the user becomes Proposal Admin and receives an Admin an Signatory token.
|
||||
With this power the Admin can add other Signatories to the Proposal.
|
||||
These Signatories can then add commands to the Proposal and/or sign off on the Proposal.
|
||||
Once all Signatories have signed off on the Proposal the Proposal leaves Draft state and enters Voting state.
|
||||
Voting state lasts as long as the Governance has it configured to last, and during this time
|
||||
people holding Community (or Council) tokens may vote on the Proposal.
|
||||
Once the Proposal is "tipped" it either enters the Defeated or Executing state.
|
||||
If Executed, it enters Completed state once all commands have been run.
|
||||
|
||||
A command can be run by any one at any time after the `instruction_hold_up_time` length has transpired on the given command.
|
||||
|
||||
### SingleSignerInstruction
|
||||
|
||||
We only support one kind of executable command right now, and this is the `SingleSignerInstruction` type.
|
||||
A Proposal can have a certain number of these, and they run independently of each other.
|
||||
These contain the actual data for a command, and how long after the voting phase a user must wait before they can be executed.
|
||||
|
||||
### Voting Dynamics
|
||||
|
||||
When a Proposal is created and signed by its Signatories voters can start voting on it using their voting weight,
|
||||
equal to deposited governing tokens into the realm. A vote is tipped once it passes the defined `vote_threshold` of votes
|
||||
and enters Succeeded or Defeated state. If Succeeded then Proposal instructions can be executed after they hold_up_time passes.
|
||||
|
||||
Users can relinquish their vote any time during Proposal lifetime, but once Proposal it tipped their vote can't be changed.
|
||||
|
||||
### Community and Councils governing tokens
|
||||
|
||||
Each Governance Realm that gets created has the option to also have a Council mint.
|
||||
A council mint is simply a separate mint from the Community mint.
|
||||
What this means is that users can submit Proposals that have a different voting population from a different mint
|
||||
that can affect the same program. A practical application of this policy may be to have a very large population control
|
||||
major version bumps of Solana via normal SOL, for instance, but hot fixes be controlled via Council tokens,
|
||||
of which there may be only 30, and which may be themselves minted and distributed via proposals by the governing population.
|
||||
|
||||
### Proposal Workflow
|
||||
|
||||

|
|
@ -1,33 +0,0 @@
|
|||
[package]
|
||||
name = "spl-governance"
|
||||
version = "0.1.0"
|
||||
description = "Solana Program Library Governance"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
arrayref = "0.3.6"
|
||||
bincode = "1.3.2"
|
||||
borsh = "0.8.1"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
serde = "1.0.121"
|
||||
serde_derive = "1.0.103"
|
||||
solana-program = "1.6.7"
|
||||
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
proptest = "0.10"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
|
@ -1,2 +0,0 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -1,22 +0,0 @@
|
|||
//! Program entrypoint definitions
|
||||
#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))]
|
||||
|
||||
use crate::{error::GovernanceError, processor};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
|
||||
program_error::PrintProgramError, pubkey::Pubkey,
|
||||
};
|
||||
|
||||
entrypoint!(process_instruction);
|
||||
fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
instruction_data: &[u8],
|
||||
) -> ProgramResult {
|
||||
if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) {
|
||||
// catch the error so we can print it
|
||||
error.print::<GovernanceError>();
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
//! Error types
|
||||
|
||||
use num_derive::FromPrimitive;
|
||||
use solana_program::{
|
||||
decode_error::DecodeError,
|
||||
msg,
|
||||
program_error::{PrintProgramError, ProgramError},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that may be returned by the Governance program
|
||||
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
|
||||
pub enum GovernanceError {
|
||||
/// Invalid instruction passed to program
|
||||
#[error("Invalid instruction passed to program")]
|
||||
InvalidInstruction,
|
||||
|
||||
/// Realm with the given name and governing mints already exists
|
||||
#[error("Realm with the given name and governing mints already exists")]
|
||||
RealmAlreadyExists,
|
||||
|
||||
/// Invalid Realm
|
||||
#[error("Invalid realm")]
|
||||
InvalidRealm,
|
||||
|
||||
/// Invalid Governing Token Mint
|
||||
#[error("Invalid Governing Token Mint")]
|
||||
InvalidGoverningTokenMint,
|
||||
|
||||
/// Governing Token Owner must sign transaction
|
||||
#[error("Governing Token Owner must sign transaction")]
|
||||
GoverningTokenOwnerMustSign,
|
||||
|
||||
/// Governing Token Owner or Delegate must sign transaction
|
||||
#[error("Governing Token Owner or Delegate must sign transaction")]
|
||||
GoverningTokenOwnerOrDelegateMustSign,
|
||||
|
||||
/// All active votes must be relinquished to withdraw governing tokens
|
||||
#[error("All active votes must be relinquished to withdraw governing tokens")]
|
||||
CannotWithdrawGoverningTokensWhenActiveVotesExist,
|
||||
|
||||
/// Invalid Token Owner Record account address
|
||||
#[error("Invalid Token Owner Record account address")]
|
||||
InvalidTokenOwnerRecordAccountAddress,
|
||||
|
||||
/// Invalid Token Owner Record Governing mint
|
||||
#[error("Invalid Token Owner Record Governing mint")]
|
||||
InvalidTokenOwnerRecordGoverningMint,
|
||||
|
||||
/// Invalid Token Owner Record Realm
|
||||
#[error("Invalid Token Owner Record Realm")]
|
||||
InvalidTokenOwnerRecordRealm,
|
||||
|
||||
/// Invalid Signatory account address
|
||||
#[error("Invalid Signatory account address")]
|
||||
InvalidSignatoryAddress,
|
||||
|
||||
/// Signatory already signed off
|
||||
#[error("Signatory already signed off")]
|
||||
SignatoryAlreadySignedOff,
|
||||
|
||||
/// Signatory must sign
|
||||
#[error("Signatory must sign")]
|
||||
SignatoryMustSign,
|
||||
|
||||
/// Invalid Proposal Owner
|
||||
#[error("Invalid Proposal Owner")]
|
||||
InvalidProposalOwnerAccount,
|
||||
|
||||
/// Invalid Governance config
|
||||
#[error("Invalid Governance config")]
|
||||
InvalidGovernanceConfig,
|
||||
|
||||
/// Proposal for the given Governance, Governing Token Mint and index already exists
|
||||
#[error("Proposal for the given Governance, Governing Token Mint and index already exists")]
|
||||
ProposalAlreadyExists,
|
||||
|
||||
/// Owner doesn't have enough governing tokens to create Proposal
|
||||
#[error("Owner doesn't have enough governing tokens to create Proposal")]
|
||||
NotEnoughTokensToCreateProposal,
|
||||
|
||||
/// Invalid State: Can't edit Signatories
|
||||
#[error("Invalid State: Can't edit Signatories")]
|
||||
InvalidStateCannotEditSignatories,
|
||||
|
||||
/// Invalid State: Can't sign off
|
||||
#[error("Invalid State: Can't sign off")]
|
||||
InvalidStateCannotSignOff,
|
||||
|
||||
/// Invalid Signatory Mint
|
||||
#[error("Invalid Signatory Mint")]
|
||||
InvalidSignatoryMint,
|
||||
|
||||
/// ---- Account Tools Errors ----
|
||||
|
||||
/// Invalid account owner
|
||||
#[error("Invalid account owner")]
|
||||
InvalidAccountOwner,
|
||||
|
||||
/// Invalid Account type
|
||||
#[error("Invalid Account type")]
|
||||
InvalidAccountType,
|
||||
|
||||
/// ---- Token Tools Errors ----
|
||||
|
||||
/// Invalid Token account owner
|
||||
#[error("Invalid Token account owner")]
|
||||
InvalidTokenAccountOwner,
|
||||
|
||||
/// ---- Bpf Upgradable Loader Tools Errors ----
|
||||
|
||||
/// Invalid ProgramData account Address
|
||||
#[error("Invalid ProgramData account address")]
|
||||
InvalidProgramDataAccountAddress,
|
||||
|
||||
/// Invalid ProgramData account data
|
||||
#[error("Invalid ProgramData account Data")]
|
||||
InvalidProgramDataAccountData,
|
||||
|
||||
/// Provided upgrade authority doesn't match current program upgrade authority
|
||||
#[error("Provided upgrade authority doesn't match current program upgrade authority")]
|
||||
InvalidUpgradeAuthority,
|
||||
|
||||
/// Current program upgrade authority must sign transaction
|
||||
#[error("Current program upgrade authority must sign transaction")]
|
||||
UpgradeAuthorityMustSign,
|
||||
|
||||
/// Given program is not upgradable
|
||||
#[error("Given program is not upgradable")]
|
||||
ProgramNotUpgradable,
|
||||
}
|
||||
|
||||
impl PrintProgramError for GovernanceError {
|
||||
fn print<E>(&self) {
|
||||
msg!("GOVERNANCE-ERROR: {}", &self.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GovernanceError> for ProgramError {
|
||||
fn from(e: GovernanceError) -> Self {
|
||||
ProgramError::Custom(e as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DecodeError<T> for GovernanceError {
|
||||
fn type_of() -> &'static str {
|
||||
"Governance Error"
|
||||
}
|
||||
}
|
|
@ -1,617 +0,0 @@
|
|||
//! Program instructions
|
||||
|
||||
use crate::{
|
||||
id,
|
||||
state::{
|
||||
governance::{
|
||||
get_account_governance_address, get_program_governance_address, GovernanceConfig,
|
||||
},
|
||||
proposal::get_proposal_address,
|
||||
realm::{get_governing_token_holding_address, get_realm_address},
|
||||
signatory_record::get_signatory_record_address,
|
||||
single_signer_instruction::InstructionData,
|
||||
token_owner_record::get_token_owner_record_address,
|
||||
},
|
||||
tools::bpf_loader_upgradeable::get_program_data_address,
|
||||
};
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
bpf_loader_upgradeable,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
system_program, sysvar,
|
||||
};
|
||||
|
||||
/// Yes/No Vote
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum Vote {
|
||||
/// Yes vote
|
||||
Yes,
|
||||
/// No vote
|
||||
No,
|
||||
}
|
||||
|
||||
/// Instructions supported by the Governance program
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
#[repr(C)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum GovernanceInstruction {
|
||||
/// Creates Governance Realm account which aggregates governances for given Community Mint and optional Council Mint
|
||||
///
|
||||
/// 0. `[writable]` Governance Realm account. PDA seeds:['governance',name]
|
||||
/// 1. `[]` Community Token Mint
|
||||
/// 2. `[writable]` Community Token Holding account. PDA seeds: ['governance',realm,community_mint]
|
||||
/// The account will be created with the Realm PDA as its owner
|
||||
/// 3. `[signer]` Payer
|
||||
/// 4. `[]` System
|
||||
/// 5. `[]` SPL Token
|
||||
/// 6. `[]` Sysvar Rent
|
||||
/// 7. `[]` Council Token Mint - optional
|
||||
/// 8. `[writable]` Council Token Holding account - optional. . PDA seeds: ['governance',realm,council_mint]
|
||||
/// The account will be created with the Realm PDA as its owner
|
||||
CreateRealm {
|
||||
#[allow(dead_code)]
|
||||
/// UTF-8 encoded Governance Realm name
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Deposits governing tokens (Community or Council) to Governance Realm and establishes your voter weight to be used for voting within the Realm
|
||||
/// Note: If subsequent (top up) deposit is made and there are active votes for the Voter then the vote weights won't be updated automatically
|
||||
/// It can be done by relinquishing votes on active Proposals and voting again with the new weight
|
||||
///
|
||||
/// 0. `[]` Governance Realm account
|
||||
/// 1. `[writable]` Governing Token Holding account. PDA seeds: ['governance',realm, governing_token_mint]
|
||||
/// 2. `[writable]` Governing Token Source account. All tokens from the account will be transferred to the Holding account
|
||||
/// 3. `[signer]` Governing Token Owner account
|
||||
/// 4. `[signer]` Governing Token Transfer authority
|
||||
/// 5. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
|
||||
/// 6. `[signer]` Payer
|
||||
/// 7. `[]` System
|
||||
/// 8. `[]` SPL Token
|
||||
/// 9. `[]` Sysvar Rent
|
||||
DepositGoverningTokens {},
|
||||
|
||||
/// Withdraws governing tokens (Community or Council) from Governance Realm and downgrades your voter weight within the Realm
|
||||
/// Note: It's only possible to withdraw tokens if the Voter doesn't have any outstanding active votes
|
||||
/// If there are any outstanding votes then they must be relinquished before tokens could be withdrawn
|
||||
///
|
||||
/// 0. `[]` Governance Realm account
|
||||
/// 1. `[writable]` Governing Token Holding account. PDA seeds: ['governance',realm, governing_token_mint]
|
||||
/// 2. `[writable]` Governing Token Destination account. All tokens will be transferred to this account
|
||||
/// 3. `[signer]` Governing Token Owner account
|
||||
/// 4. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
|
||||
/// 5. `[]` SPL Token
|
||||
WithdrawGoverningTokens {},
|
||||
|
||||
/// Sets Governance Delegate for the given Realm and Governing Token Mint (Community or Council)
|
||||
/// The Delegate would have voting rights and could vote on behalf of the Governing Token Owner
|
||||
/// The Delegate would also be able to create Proposals on behalf of the Governing Token Owner
|
||||
/// Note: This doesn't take voting rights from the Token Owner who still can vote and change governance_delegate
|
||||
///
|
||||
/// 0. `[signer]` Current Governance Delegate or Governing Token owner
|
||||
/// 1. `[writable]` Token Owner Record
|
||||
SetGovernanceDelegate {
|
||||
#[allow(dead_code)]
|
||||
/// New Governance Delegate
|
||||
new_governance_delegate: Option<Pubkey>,
|
||||
},
|
||||
|
||||
/// Creates Account Governance account which can be used to govern an arbitrary account
|
||||
///
|
||||
/// 0. `[]` Realm account the created Governance belongs to
|
||||
/// 1. `[writable]` Account Governance account. PDA seeds: ['account-governance', realm, governed_account]
|
||||
/// 2. `[signer]` Payer
|
||||
/// 3. `[]` System program
|
||||
/// 4. `[]` Sysvar Rent
|
||||
CreateAccountGovernance {
|
||||
/// Governance config
|
||||
#[allow(dead_code)]
|
||||
config: GovernanceConfig,
|
||||
},
|
||||
|
||||
/// Creates Program Governance account which governs an upgradable program
|
||||
///
|
||||
/// 0. `[]` Realm account the created Governance belongs to
|
||||
/// 1. `[writable]` Program Governance account. PDA seeds: ['program-governance', realm, governed_program]
|
||||
/// 2. `[writable]` Program Data account of the Program governed by this Governance account
|
||||
/// 3. `[signer]` Current Upgrade Authority account of the Program governed by this Governance account
|
||||
/// 4. `[signer]` Payer
|
||||
/// 5. `[]` bpf_upgradeable_loader program
|
||||
/// 6. `[]` System program
|
||||
/// 7. `[]` Sysvar Rent
|
||||
CreateProgramGovernance {
|
||||
/// Governance config
|
||||
#[allow(dead_code)]
|
||||
config: GovernanceConfig,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Indicate whether Program's upgrade_authority should be transferred to the Governance PDA
|
||||
/// If it's set to false then it can be done at a later time
|
||||
/// However the instruction would validate the current upgrade_authority signed the transaction nonetheless
|
||||
transfer_upgrade_authority: bool,
|
||||
},
|
||||
|
||||
/// Creates Proposal account for Instructions that will be executed at various slots in the future
|
||||
///
|
||||
/// 0. `[writable]` Proposal account. PDA seeds ['governance',governance, governing_token_mint, proposal_index]
|
||||
/// 1. `[writable]` Governance account
|
||||
/// 2. `[]` Token Owner Record account
|
||||
/// 3. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||
/// 4. `[signer]` Payer
|
||||
/// 5. `[]` System program
|
||||
/// 6. `[]` Rent sysvar
|
||||
/// 7. `[]` Clock sysvar
|
||||
CreateProposal {
|
||||
#[allow(dead_code)]
|
||||
/// UTF-8 encoded name of the proposal
|
||||
name: String,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Link to gist explaining proposal
|
||||
description_link: String,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Governing Token Mint the Proposal is created for
|
||||
governing_token_mint: Pubkey,
|
||||
},
|
||||
|
||||
/// Adds a signatory to the Proposal which means this Proposal can't leave Draft state until yet another Signatory signs
|
||||
///
|
||||
/// 0. `[writable]` Proposal account
|
||||
/// 1. `[]` Token Owner Record account
|
||||
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||
/// 3. `[writable]` Signatory Record Account
|
||||
/// 4. `[signer]` Payer
|
||||
/// 5. `[]` System program
|
||||
/// 6. `[]` Rent sysvar
|
||||
AddSignatory {
|
||||
#[allow(dead_code)]
|
||||
/// Signatory to add to the Proposal
|
||||
signatory: Pubkey,
|
||||
},
|
||||
|
||||
/// Removes a Signatory from the Proposal
|
||||
///
|
||||
/// 0. `[writable]` Proposal account
|
||||
/// 1. `[]` Token Owner Record account
|
||||
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||
/// 3. `[writable]` Signatory Record Account
|
||||
/// 4. `[writable]` Beneficiary Account which would receive lamports from the disposed Signatory Record Account
|
||||
/// 5. `[]` Clock sysvar
|
||||
RemoveSignatory {
|
||||
#[allow(dead_code)]
|
||||
/// Signatory to remove from the Proposal
|
||||
signatory: Pubkey,
|
||||
},
|
||||
|
||||
/// Adds an instruction to the Proposal. Max of 5 of any type. More than 5 will throw error
|
||||
///
|
||||
/// 0. `[writable]` Proposal account
|
||||
/// 1. `[writable]` Uninitialized Proposal SingleSignerInstruction account
|
||||
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||
AddSingleSignerInstruction {
|
||||
#[allow(dead_code)]
|
||||
/// Slot waiting time between vote period ending and this being eligible for execution
|
||||
hold_up_time: u64,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Instruction
|
||||
instruction: InstructionData,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Position in instruction array
|
||||
position: u8,
|
||||
},
|
||||
|
||||
/// Remove instruction from the Proposal
|
||||
///
|
||||
/// 0. `[writable]` Proposal account
|
||||
/// 1. `[writable]` Proposal SingleSignerInstruction account
|
||||
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||
RemoveInstruction,
|
||||
|
||||
/// Update instruction hold up time in the Proposal
|
||||
///
|
||||
/// 0. `[]` Proposal account
|
||||
/// 1. `[writable]` Proposal SingleSignerInstruction account
|
||||
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||
UpdateInstructionHoldUpTime {
|
||||
#[allow(dead_code)]
|
||||
/// Minimum waiting time in slots for an instruction to be executed after proposal is voted on
|
||||
hold_up_time: u64,
|
||||
},
|
||||
|
||||
/// Cancels Proposal and moves it into Canceled
|
||||
///
|
||||
/// 0. `[writable]` Proposal account
|
||||
/// 1. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||
CancelProposal,
|
||||
|
||||
/// Signs off Proposal indicating the Signatory approves the Proposal
|
||||
/// When the last Signatory signs the Proposal state moves to Voting state
|
||||
///
|
||||
/// 0. `[writable]` Proposal account
|
||||
/// 1. `[writable]` Signatory Record account
|
||||
/// 2. `[signer]` Signatory account
|
||||
/// 3. `[]` Clock sysvar
|
||||
SignOffProposal,
|
||||
|
||||
/// Uses your voter weight (deposited Community or Council tokens) to cast a vote on a Proposal
|
||||
/// By doing so you indicate you approve or disapprove of running the Proposal set of instructions
|
||||
/// If you tip the consensus then the instructions can begin to be run after their hold up time
|
||||
///
|
||||
/// 0. `[writable]` Proposal account
|
||||
/// 1. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
|
||||
/// 2. `[writable]` Proposal Vote Record account. PDA seeds: ['governance',proposal,governing_token_owner]
|
||||
/// 3. `[signer]` Governance Authority account
|
||||
/// 4. `[]` Governance account
|
||||
Vote {
|
||||
#[allow(dead_code)]
|
||||
/// Yes/No vote
|
||||
vote: Vote,
|
||||
},
|
||||
|
||||
/// Relinquish Vote removes voter weight from a Proposal and removes it from voter's active votes
|
||||
/// If the Proposal is still being voted on then the voter's weight won't count towards the vote outcome
|
||||
/// If the Proposal is already in decided state then the instruction has no impact on the Proposal
|
||||
/// and only allows voters to prune their outstanding votes in case they wanted to withdraw Governing tokens from the Realm
|
||||
///
|
||||
/// 0. `[writable]` Proposal account
|
||||
/// 1. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
|
||||
/// 2. `[writable]` Proposal Vote Record account. PDA seeds: ['governance',proposal,governing_token_owner]
|
||||
/// 3. `[signer]` Governance Authority account
|
||||
RelinquishVote,
|
||||
|
||||
/// Executes an instruction in the Proposal
|
||||
/// Anybody can execute transaction once Proposal has been voted Yes and transaction_hold_up time has passed
|
||||
/// The actual instruction being executed will be signed by Governance PDA
|
||||
/// For example to execute Program upgrade the ProgramGovernance PDA would be used as the singer
|
||||
///
|
||||
/// 0. `[writable]` Proposal account
|
||||
/// 1. `[writable]` Instruction account you wish to execute
|
||||
/// 2. `[]` Program being invoked account
|
||||
/// 3. `[]` Governance account (PDA)
|
||||
/// 4. `[]` Clock sysvar
|
||||
/// 5+ Any extra accounts that are part of the instruction, in order
|
||||
Execute,
|
||||
}
|
||||
|
||||
/// Creates CreateRealm instruction
|
||||
pub fn create_realm(
|
||||
// Accounts
|
||||
community_token_mint: &Pubkey,
|
||||
payer: &Pubkey,
|
||||
council_token_mint: Option<Pubkey>,
|
||||
// Args
|
||||
name: String,
|
||||
) -> Instruction {
|
||||
let realm_address = get_realm_address(&name);
|
||||
let community_token_holding_address =
|
||||
get_governing_token_holding_address(&realm_address, &community_token_mint);
|
||||
|
||||
let mut accounts = vec![
|
||||
AccountMeta::new(realm_address, false),
|
||||
AccountMeta::new_readonly(*community_token_mint, false),
|
||||
AccountMeta::new(community_token_holding_address, false),
|
||||
AccountMeta::new_readonly(*payer, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
];
|
||||
|
||||
if let Some(council_token_mint) = council_token_mint {
|
||||
let council_token_holding_address =
|
||||
get_governing_token_holding_address(&realm_address, &council_token_mint);
|
||||
|
||||
accounts.push(AccountMeta::new_readonly(council_token_mint, false));
|
||||
accounts.push(AccountMeta::new(council_token_holding_address, false));
|
||||
}
|
||||
|
||||
let instruction = GovernanceInstruction::CreateRealm { name };
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates DepositGoverningTokens instruction
|
||||
pub fn deposit_governing_tokens(
|
||||
// Accounts
|
||||
realm: &Pubkey,
|
||||
governing_token_source: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
governing_token_transfer_authority: &Pubkey,
|
||||
payer: &Pubkey,
|
||||
// Args
|
||||
governing_token_mint: &Pubkey,
|
||||
) -> Instruction {
|
||||
let vote_record_address =
|
||||
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
|
||||
|
||||
let governing_token_holding_address =
|
||||
get_governing_token_holding_address(realm, governing_token_mint);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(*realm, false),
|
||||
AccountMeta::new(governing_token_holding_address, false),
|
||||
AccountMeta::new(*governing_token_source, false),
|
||||
AccountMeta::new_readonly(*governing_token_owner, true),
|
||||
AccountMeta::new_readonly(*governing_token_transfer_authority, true),
|
||||
AccountMeta::new(vote_record_address, false),
|
||||
AccountMeta::new_readonly(*payer, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::DepositGoverningTokens {};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates WithdrawGoverningTokens instruction
|
||||
pub fn withdraw_governing_tokens(
|
||||
// Accounts
|
||||
realm: &Pubkey,
|
||||
governing_token_destination: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
// Args
|
||||
governing_token_mint: &Pubkey,
|
||||
) -> Instruction {
|
||||
let vote_record_address =
|
||||
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
|
||||
|
||||
let governing_token_holding_address =
|
||||
get_governing_token_holding_address(realm, governing_token_mint);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(*realm, false),
|
||||
AccountMeta::new(governing_token_holding_address, false),
|
||||
AccountMeta::new(*governing_token_destination, false),
|
||||
AccountMeta::new_readonly(*governing_token_owner, true),
|
||||
AccountMeta::new(vote_record_address, false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::WithdrawGoverningTokens {};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates SetGovernanceDelegate instruction
|
||||
pub fn set_governance_delegate(
|
||||
// Accounts
|
||||
governance_authority: &Pubkey,
|
||||
// Args
|
||||
realm: &Pubkey,
|
||||
governing_token_mint: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
new_governance_delegate: &Option<Pubkey>,
|
||||
) -> Instruction {
|
||||
let vote_record_address =
|
||||
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(*governance_authority, true),
|
||||
AccountMeta::new(vote_record_address, false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::SetGovernanceDelegate {
|
||||
new_governance_delegate: *new_governance_delegate,
|
||||
};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates CreateAccountGovernance instruction
|
||||
pub fn create_account_governance(
|
||||
// Accounts
|
||||
payer: &Pubkey,
|
||||
// Args
|
||||
config: GovernanceConfig,
|
||||
) -> Instruction {
|
||||
let account_governance_address =
|
||||
get_account_governance_address(&config.realm, &config.governed_account);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(config.realm, false),
|
||||
AccountMeta::new(account_governance_address, false),
|
||||
AccountMeta::new_readonly(*payer, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::CreateAccountGovernance { config };
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates CreateProgramGovernance instruction
|
||||
pub fn create_program_governance(
|
||||
// Accounts
|
||||
governed_program_upgrade_authority: &Pubkey,
|
||||
payer: &Pubkey,
|
||||
// Args
|
||||
config: GovernanceConfig,
|
||||
transfer_upgrade_authority: bool,
|
||||
) -> Instruction {
|
||||
let program_governance_address =
|
||||
get_program_governance_address(&config.realm, &config.governed_account);
|
||||
let governed_program_data_address = get_program_data_address(&config.governed_account);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(config.realm, false),
|
||||
AccountMeta::new(program_governance_address, false),
|
||||
AccountMeta::new(governed_program_data_address, false),
|
||||
AccountMeta::new_readonly(*governed_program_upgrade_authority, true),
|
||||
AccountMeta::new_readonly(*payer, true),
|
||||
AccountMeta::new_readonly(bpf_loader_upgradeable::id(), false),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::CreateProgramGovernance {
|
||||
config,
|
||||
transfer_upgrade_authority,
|
||||
};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates CreateProposal instruction
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_proposal(
|
||||
// Accounts
|
||||
governance: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
governance_authority: &Pubkey,
|
||||
payer: &Pubkey,
|
||||
// Args
|
||||
realm: &Pubkey,
|
||||
name: String,
|
||||
description_link: String,
|
||||
governing_token_mint: &Pubkey,
|
||||
proposal_index: u16,
|
||||
) -> Instruction {
|
||||
let proposal_address = get_proposal_address(
|
||||
governance,
|
||||
governing_token_mint,
|
||||
&proposal_index.to_le_bytes(),
|
||||
);
|
||||
let token_owner_record_address =
|
||||
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new(proposal_address, false),
|
||||
AccountMeta::new(*governance, false),
|
||||
AccountMeta::new_readonly(token_owner_record_address, false),
|
||||
AccountMeta::new_readonly(*governance_authority, true),
|
||||
AccountMeta::new_readonly(*payer, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::CreateProposal {
|
||||
name,
|
||||
description_link,
|
||||
governing_token_mint: *governing_token_mint,
|
||||
};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates AddSignatory instruction
|
||||
pub fn add_signatory(
|
||||
// Accounts
|
||||
proposal: &Pubkey,
|
||||
token_owner_record: &Pubkey,
|
||||
governance_authority: &Pubkey,
|
||||
payer: &Pubkey,
|
||||
// Args
|
||||
signatory: &Pubkey,
|
||||
) -> Instruction {
|
||||
let signatory_record_address = get_signatory_record_address(proposal, signatory);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*proposal, false),
|
||||
AccountMeta::new_readonly(*token_owner_record, false),
|
||||
AccountMeta::new_readonly(*governance_authority, true),
|
||||
AccountMeta::new(signatory_record_address, false),
|
||||
AccountMeta::new_readonly(*payer, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::AddSignatory {
|
||||
signatory: *signatory,
|
||||
};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates RemoveSignatory instruction
|
||||
pub fn remove_signatory(
|
||||
// Accounts
|
||||
proposal: &Pubkey,
|
||||
token_owner_record: &Pubkey,
|
||||
governance_authority: &Pubkey,
|
||||
signatory: &Pubkey,
|
||||
beneficiary: &Pubkey,
|
||||
) -> Instruction {
|
||||
let signatory_record_address = get_signatory_record_address(proposal, signatory);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*proposal, false),
|
||||
AccountMeta::new_readonly(*token_owner_record, false),
|
||||
AccountMeta::new_readonly(*governance_authority, true),
|
||||
AccountMeta::new(signatory_record_address, false),
|
||||
AccountMeta::new(*beneficiary, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::RemoveSignatory {
|
||||
signatory: *signatory,
|
||||
};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates SignOffProposal instruction
|
||||
pub fn sign_off_proposal(
|
||||
// Accounts
|
||||
proposal: &Pubkey,
|
||||
signatory: &Pubkey,
|
||||
) -> Instruction {
|
||||
let signatory_record_address = get_signatory_record_address(proposal, signatory);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*proposal, false),
|
||||
AccountMeta::new(signatory_record_address, false),
|
||||
AccountMeta::new_readonly(*signatory, true),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::SignOffProposal;
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
#![deny(missing_docs)]
|
||||
//! A Governance program for the Solana blockchain.
|
||||
|
||||
pub mod entrypoint;
|
||||
pub mod error;
|
||||
pub mod instruction;
|
||||
pub mod processor;
|
||||
pub mod state;
|
||||
pub mod tools;
|
||||
|
||||
// Export current sdk types for downstream users building with a different sdk version
|
||||
pub use solana_program;
|
||||
|
||||
solana_program::declare_id!("GovernancerdmUu324nahyv33G5poQdLUEZ1nEytDeP");
|
||||
|
||||
/// Seed prefix for Governance PDAs
|
||||
pub const PROGRAM_AUTHORITY_SEED: &[u8] = b"governance";
|
|
@ -1,94 +0,0 @@
|
|||
//! Program processor
|
||||
|
||||
mod process_add_signatory;
|
||||
mod process_create_account_governance;
|
||||
mod process_create_program_governance;
|
||||
mod process_create_proposal;
|
||||
mod process_create_realm;
|
||||
mod process_deposit_governing_tokens;
|
||||
mod process_remove_signatory;
|
||||
mod process_set_governance_delegate;
|
||||
mod process_sign_off_proposal;
|
||||
mod process_withdraw_governing_tokens;
|
||||
|
||||
use crate::instruction::GovernanceInstruction;
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
use process_add_signatory::*;
|
||||
use process_create_account_governance::*;
|
||||
use process_create_program_governance::*;
|
||||
use process_create_proposal::*;
|
||||
use process_create_realm::*;
|
||||
use process_deposit_governing_tokens::*;
|
||||
use process_remove_signatory::*;
|
||||
use process_set_governance_delegate::*;
|
||||
use process_sign_off_proposal::*;
|
||||
use process_withdraw_governing_tokens::*;
|
||||
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
/// Processes an instruction
|
||||
pub fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
input: &[u8],
|
||||
) -> ProgramResult {
|
||||
let instruction = GovernanceInstruction::try_from_slice(input)
|
||||
.map_err(|_| ProgramError::InvalidInstructionData)?;
|
||||
|
||||
msg!("GOVERNANCE-INSTRUCTION: {:?}", instruction);
|
||||
|
||||
match instruction {
|
||||
GovernanceInstruction::CreateRealm { name } => {
|
||||
process_create_realm(program_id, accounts, name)
|
||||
}
|
||||
|
||||
GovernanceInstruction::DepositGoverningTokens {} => {
|
||||
process_deposit_governing_tokens(program_id, accounts)
|
||||
}
|
||||
|
||||
GovernanceInstruction::WithdrawGoverningTokens {} => {
|
||||
process_withdraw_governing_tokens(program_id, accounts)
|
||||
}
|
||||
|
||||
GovernanceInstruction::SetGovernanceDelegate {
|
||||
new_governance_delegate,
|
||||
} => process_set_governance_delegate(accounts, &new_governance_delegate),
|
||||
GovernanceInstruction::CreateProgramGovernance {
|
||||
config,
|
||||
transfer_upgrade_authority,
|
||||
} => process_create_program_governance(
|
||||
program_id,
|
||||
accounts,
|
||||
config,
|
||||
transfer_upgrade_authority,
|
||||
),
|
||||
GovernanceInstruction::CreateAccountGovernance { config } => {
|
||||
process_create_account_governance(program_id, accounts, config)
|
||||
}
|
||||
GovernanceInstruction::CreateProposal {
|
||||
name,
|
||||
description_link,
|
||||
governing_token_mint,
|
||||
} => process_create_proposal(
|
||||
program_id,
|
||||
accounts,
|
||||
name,
|
||||
description_link,
|
||||
governing_token_mint,
|
||||
),
|
||||
GovernanceInstruction::AddSignatory { signatory } => {
|
||||
process_add_signatory(program_id, accounts, signatory)
|
||||
}
|
||||
GovernanceInstruction::RemoveSignatory { signatory } => {
|
||||
process_remove_signatory(program_id, accounts, signatory)
|
||||
}
|
||||
GovernanceInstruction::SignOffProposal {} => {
|
||||
process_sign_off_proposal(program_id, accounts)
|
||||
}
|
||||
_ => todo!("Instruction not implemented yet"),
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
state::{
|
||||
enums::GovernanceAccountType,
|
||||
proposal::deserialize_proposal_raw,
|
||||
signatory_record::{get_signatory_record_address_seeds, SignatoryRecord},
|
||||
token_owner_record::deserialize_token_owner_record_for_proposal_owner,
|
||||
},
|
||||
tools::{
|
||||
account::create_and_serialize_account_signed,
|
||||
asserts::assert_token_owner_or_delegate_is_signer,
|
||||
},
|
||||
};
|
||||
|
||||
/// Processes AddSignatory instruction
|
||||
pub fn process_add_signatory(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
signatory: Pubkey,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let proposal_info = next_account_info(account_info_iter)?; // 0
|
||||
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
|
||||
let governance_authority_info = next_account_info(account_info_iter)?; // 2
|
||||
|
||||
let signatory_record_info = next_account_info(account_info_iter)?; // 3
|
||||
|
||||
let payer_info = next_account_info(account_info_iter)?; // 4
|
||||
let system_info = next_account_info(account_info_iter)?; // 5
|
||||
|
||||
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
|
||||
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||
|
||||
let mut proposal_data = deserialize_proposal_raw(proposal_info)?;
|
||||
proposal_data.assert_can_edit_signatories()?;
|
||||
|
||||
let token_owner_record_data = deserialize_token_owner_record_for_proposal_owner(
|
||||
token_owner_record_info,
|
||||
&proposal_data.token_owner_record,
|
||||
)?;
|
||||
|
||||
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?;
|
||||
|
||||
let signatory_record_data = SignatoryRecord {
|
||||
account_type: GovernanceAccountType::SignatoryRecord,
|
||||
proposal: *proposal_info.key,
|
||||
signatory,
|
||||
signed_off: false,
|
||||
};
|
||||
|
||||
create_and_serialize_account_signed::<SignatoryRecord>(
|
||||
payer_info,
|
||||
signatory_record_info,
|
||||
&signatory_record_data,
|
||||
&get_signatory_record_address_seeds(proposal_info.key, &signatory),
|
||||
program_id,
|
||||
system_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
proposal_data.signatories_count = proposal_data.signatories_count.checked_add(1).unwrap();
|
||||
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use crate::{
|
||||
state::{
|
||||
enums::GovernanceAccountType,
|
||||
governance::{
|
||||
assert_is_valid_governance_config, get_account_governance_address_seeds, Governance,
|
||||
GovernanceConfig,
|
||||
},
|
||||
},
|
||||
tools::account::create_and_serialize_account_signed,
|
||||
};
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
/// Processes CreateAccountGovernance instruction
|
||||
pub fn process_create_account_governance(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
config: GovernanceConfig,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let realm_info = next_account_info(account_info_iter)?; // 0
|
||||
let account_governance_info = next_account_info(account_info_iter)?; // 0
|
||||
let payer_info = next_account_info(account_info_iter)?; // 1
|
||||
let system_info = next_account_info(account_info_iter)?; // 2
|
||||
|
||||
let rent_sysvar_info = next_account_info(account_info_iter)?; // 3
|
||||
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||
|
||||
assert_is_valid_governance_config(&config, realm_info)?;
|
||||
|
||||
let account_governance_data = Governance {
|
||||
account_type: GovernanceAccountType::AccountGovernance,
|
||||
config: config.clone(),
|
||||
proposals_count: 0,
|
||||
};
|
||||
|
||||
create_and_serialize_account_signed::<Governance>(
|
||||
payer_info,
|
||||
&account_governance_info,
|
||||
&account_governance_data,
|
||||
&get_account_governance_address_seeds(&config.realm, &config.governed_account),
|
||||
program_id,
|
||||
system_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use crate::{
|
||||
state::governance::Governance,
|
||||
state::{
|
||||
enums::GovernanceAccountType,
|
||||
governance::{
|
||||
assert_is_valid_governance_config, get_program_governance_address_seeds,
|
||||
GovernanceConfig,
|
||||
},
|
||||
},
|
||||
tools::{
|
||||
account::create_and_serialize_account_signed,
|
||||
bpf_loader_upgradeable::{
|
||||
assert_program_upgrade_authority_is_signer, set_program_upgrade_authority,
|
||||
},
|
||||
},
|
||||
};
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
/// Processes CreateProgramGovernance instruction
|
||||
pub fn process_create_program_governance(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
config: GovernanceConfig,
|
||||
transfer_upgrade_authority: bool,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let realm_info = next_account_info(account_info_iter)?; // 0
|
||||
let program_governance_info = next_account_info(account_info_iter)?; // 0
|
||||
|
||||
let governed_program_data_info = next_account_info(account_info_iter)?; // 1
|
||||
let governed_program_upgrade_authority_info = next_account_info(account_info_iter)?; // 2
|
||||
|
||||
let payer_info = next_account_info(account_info_iter)?; // 3
|
||||
let bpf_upgrade_loader_info = next_account_info(account_info_iter)?; // 4
|
||||
|
||||
let system_info = next_account_info(account_info_iter)?; // 5
|
||||
|
||||
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
|
||||
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||
|
||||
assert_is_valid_governance_config(&config, &realm_info)?;
|
||||
|
||||
let program_governance_data = Governance {
|
||||
account_type: GovernanceAccountType::ProgramGovernance,
|
||||
config: config.clone(),
|
||||
proposals_count: 0,
|
||||
};
|
||||
|
||||
create_and_serialize_account_signed::<Governance>(
|
||||
payer_info,
|
||||
&program_governance_info,
|
||||
&program_governance_data,
|
||||
&get_program_governance_address_seeds(&config.realm, &config.governed_account),
|
||||
program_id,
|
||||
system_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
if transfer_upgrade_authority {
|
||||
set_program_upgrade_authority(
|
||||
&config.governed_account,
|
||||
governed_program_data_info,
|
||||
governed_program_upgrade_authority_info,
|
||||
program_governance_info,
|
||||
bpf_upgrade_loader_info,
|
||||
)?;
|
||||
} else {
|
||||
assert_program_upgrade_authority_is_signer(
|
||||
&config.governed_account,
|
||||
&governed_program_data_info,
|
||||
&governed_program_upgrade_authority_info,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
clock::Clock,
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
state::{
|
||||
enums::{GovernanceAccountType, ProposalState},
|
||||
governance::deserialize_governance_raw,
|
||||
proposal::{get_proposal_address_seeds, Proposal},
|
||||
token_owner_record::deserialize_token_owner_record_for_realm_and_governing_mint,
|
||||
},
|
||||
tools::{
|
||||
account::create_and_serialize_account_signed,
|
||||
asserts::assert_token_owner_or_delegate_is_signer,
|
||||
},
|
||||
};
|
||||
|
||||
/// Processes CreateProposal instruction
|
||||
pub fn process_create_proposal(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
name: String,
|
||||
description_link: String,
|
||||
governing_token_mint: Pubkey,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let proposal_info = next_account_info(account_info_iter)?; // 0
|
||||
let governance_info = next_account_info(account_info_iter)?; // 1
|
||||
|
||||
let token_owner_record_info = next_account_info(account_info_iter)?; // 2
|
||||
let governance_authority_info = next_account_info(account_info_iter)?; // 3
|
||||
|
||||
let payer_info = next_account_info(account_info_iter)?; // 4
|
||||
let system_info = next_account_info(account_info_iter)?; // 5
|
||||
|
||||
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
|
||||
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||
|
||||
let clock_info = next_account_info(account_info_iter)?; // 7
|
||||
let clock = Clock::from_account_info(clock_info)?;
|
||||
|
||||
if !proposal_info.data_is_empty() {
|
||||
return Err(GovernanceError::ProposalAlreadyExists.into());
|
||||
}
|
||||
|
||||
let mut governance_data = deserialize_governance_raw(governance_info)?;
|
||||
|
||||
let token_owner_record_data = deserialize_token_owner_record_for_realm_and_governing_mint(
|
||||
&token_owner_record_info,
|
||||
&governance_data.config.realm,
|
||||
&governing_token_mint,
|
||||
)?;
|
||||
|
||||
// proposal_owner must be either governing token owner or governance_delegate and must sign this transaction
|
||||
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?;
|
||||
|
||||
if token_owner_record_data.governing_token_deposit_amount
|
||||
< governance_data.config.min_tokens_to_create_proposal as u64
|
||||
{
|
||||
return Err(GovernanceError::NotEnoughTokensToCreateProposal.into());
|
||||
}
|
||||
|
||||
let proposal_data = Proposal {
|
||||
account_type: GovernanceAccountType::Proposal,
|
||||
governance: *governance_info.key,
|
||||
governing_token_mint,
|
||||
state: ProposalState::Draft,
|
||||
token_owner_record: *token_owner_record_info.key,
|
||||
|
||||
signatories_count: 0,
|
||||
signatories_signed_off_count: 0,
|
||||
|
||||
name,
|
||||
description_link,
|
||||
|
||||
draft_at: clock.slot,
|
||||
signing_off_at: None,
|
||||
voting_at: None,
|
||||
voting_completed_at: None,
|
||||
executing_at: None,
|
||||
closed_at: None,
|
||||
|
||||
number_of_executed_instructions: 0,
|
||||
number_of_instructions: 0,
|
||||
};
|
||||
|
||||
create_and_serialize_account_signed::<Proposal>(
|
||||
payer_info,
|
||||
proposal_info,
|
||||
&proposal_data,
|
||||
&get_proposal_address_seeds(
|
||||
governance_info.key,
|
||||
&governing_token_mint,
|
||||
&governance_data.proposals_count.to_le_bytes(),
|
||||
),
|
||||
program_id,
|
||||
system_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
governance_data.proposals_count = governance_data.proposals_count.checked_add(1).unwrap();
|
||||
governance_data.serialize(&mut *governance_info.data.borrow_mut())?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
state::{
|
||||
enums::GovernanceAccountType,
|
||||
realm::{get_governing_token_holding_address_seeds, get_realm_address_seeds, Realm},
|
||||
},
|
||||
tools::{account::create_and_serialize_account_signed, token::create_spl_token_account_signed},
|
||||
};
|
||||
|
||||
/// Processes CreateRealm instruction
|
||||
pub fn process_create_realm(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
name: String,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let realm_info = next_account_info(account_info_iter)?; // 0
|
||||
let governance_token_mint_info = next_account_info(account_info_iter)?; // 1
|
||||
let governance_token_holding_info = next_account_info(account_info_iter)?; // 2
|
||||
let payer_info = next_account_info(account_info_iter)?; // 3
|
||||
let system_info = next_account_info(account_info_iter)?; // 4
|
||||
let spl_token_info = next_account_info(account_info_iter)?; // 5
|
||||
|
||||
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
|
||||
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||
|
||||
if !realm_info.data_is_empty() {
|
||||
return Err(GovernanceError::RealmAlreadyExists.into());
|
||||
}
|
||||
|
||||
create_spl_token_account_signed(
|
||||
payer_info,
|
||||
governance_token_holding_info,
|
||||
&get_governing_token_holding_address_seeds(realm_info.key, governance_token_mint_info.key),
|
||||
governance_token_mint_info,
|
||||
realm_info,
|
||||
program_id,
|
||||
system_info,
|
||||
spl_token_info,
|
||||
rent_sysvar_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
let council_token_mint_address = if let Ok(council_token_mint_info) =
|
||||
next_account_info(account_info_iter)
|
||||
// 7
|
||||
{
|
||||
let council_token_holding_info = next_account_info(account_info_iter)?; //8
|
||||
|
||||
create_spl_token_account_signed(
|
||||
payer_info,
|
||||
council_token_holding_info,
|
||||
&get_governing_token_holding_address_seeds(realm_info.key, council_token_mint_info.key),
|
||||
council_token_mint_info,
|
||||
realm_info,
|
||||
program_id,
|
||||
system_info,
|
||||
spl_token_info,
|
||||
rent_sysvar_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
Some(*council_token_mint_info.key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let realm_data = Realm {
|
||||
account_type: GovernanceAccountType::Realm,
|
||||
community_mint: *governance_token_mint_info.key,
|
||||
council_mint: council_token_mint_address,
|
||||
name: name.clone(),
|
||||
};
|
||||
|
||||
create_and_serialize_account_signed::<Realm>(
|
||||
payer_info,
|
||||
&realm_info,
|
||||
&realm_data,
|
||||
&get_realm_address_seeds(&name),
|
||||
program_id,
|
||||
system_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
state::{
|
||||
enums::GovernanceAccountType,
|
||||
realm::deserialize_realm_raw,
|
||||
token_owner_record::{
|
||||
deserialize_token_owner_record, get_token_owner_record_address_seeds, TokenOwnerRecord,
|
||||
},
|
||||
},
|
||||
tools::{
|
||||
account::create_and_serialize_account_signed,
|
||||
token::{
|
||||
get_amount_from_token_account, get_mint_from_token_account,
|
||||
get_owner_from_token_account, transfer_spl_tokens,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// Processes DepositGoverningTokens instruction
|
||||
pub fn process_deposit_governing_tokens(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let realm_info = next_account_info(account_info_iter)?; // 0
|
||||
let governing_token_holding_info = next_account_info(account_info_iter)?; // 1
|
||||
let governing_token_source_info = next_account_info(account_info_iter)?; // 2
|
||||
let governing_token_owner_info = next_account_info(account_info_iter)?; // 3
|
||||
let governing_token_transfer_authority_info = next_account_info(account_info_iter)?; // 4
|
||||
let token_owner_record_info = next_account_info(account_info_iter)?; // 5
|
||||
let payer_info = next_account_info(account_info_iter)?; // 6
|
||||
let system_info = next_account_info(account_info_iter)?; // 7
|
||||
let spl_token_info = next_account_info(account_info_iter)?; // 8
|
||||
|
||||
let rent_sysvar_info = next_account_info(account_info_iter)?; // 9
|
||||
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||
|
||||
let realm_data = deserialize_realm_raw(realm_info)?;
|
||||
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
|
||||
|
||||
realm_data.assert_is_valid_governing_token_mint(&governing_token_mint)?;
|
||||
|
||||
let amount = get_amount_from_token_account(governing_token_source_info)?;
|
||||
|
||||
transfer_spl_tokens(
|
||||
&governing_token_source_info,
|
||||
&governing_token_holding_info,
|
||||
&governing_token_transfer_authority_info,
|
||||
amount,
|
||||
spl_token_info,
|
||||
)?;
|
||||
|
||||
let token_owner_record_address_seeds = get_token_owner_record_address_seeds(
|
||||
realm_info.key,
|
||||
&governing_token_mint,
|
||||
governing_token_owner_info.key,
|
||||
);
|
||||
|
||||
if token_owner_record_info.data_is_empty() {
|
||||
// Deposited tokens can only be withdrawn by the owner so let's make sure the owner signed the transaction
|
||||
let governing_token_owner = get_owner_from_token_account(&governing_token_source_info)?;
|
||||
|
||||
if !(governing_token_owner == *governing_token_owner_info.key
|
||||
&& governing_token_owner_info.is_signer)
|
||||
{
|
||||
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
||||
|
||||
let token_owner_record_data = TokenOwnerRecord {
|
||||
account_type: GovernanceAccountType::TokenOwnerRecord,
|
||||
realm: *realm_info.key,
|
||||
governing_token_owner: *governing_token_owner_info.key,
|
||||
governing_token_deposit_amount: amount,
|
||||
governing_token_mint,
|
||||
governance_delegate: None,
|
||||
active_votes_count: 0,
|
||||
total_votes_count: 0,
|
||||
};
|
||||
|
||||
create_and_serialize_account_signed(
|
||||
payer_info,
|
||||
token_owner_record_info,
|
||||
&token_owner_record_data,
|
||||
&token_owner_record_address_seeds,
|
||||
program_id,
|
||||
system_info,
|
||||
rent,
|
||||
)?;
|
||||
} else {
|
||||
let mut token_owner_record_data = deserialize_token_owner_record(
|
||||
token_owner_record_info,
|
||||
&token_owner_record_address_seeds,
|
||||
)?;
|
||||
|
||||
token_owner_record_data.governing_token_deposit_amount = token_owner_record_data
|
||||
.governing_token_deposit_amount
|
||||
.checked_add(amount)
|
||||
.unwrap();
|
||||
|
||||
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
clock::Clock,
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
state::{
|
||||
enums::ProposalState, proposal::deserialize_proposal_raw,
|
||||
signatory_record::deserialize_signatory_record,
|
||||
token_owner_record::deserialize_token_owner_record_for_proposal_owner,
|
||||
},
|
||||
tools::{account::dispose_account, asserts::assert_token_owner_or_delegate_is_signer},
|
||||
};
|
||||
|
||||
/// Processes RemoveSignatory instruction
|
||||
pub fn process_remove_signatory(
|
||||
_program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
signatory: Pubkey,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let proposal_info = next_account_info(account_info_iter)?; // 0
|
||||
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
|
||||
let governance_authority_info = next_account_info(account_info_iter)?; // 2
|
||||
|
||||
let signatory_record_info = next_account_info(account_info_iter)?; // 3
|
||||
let beneficiary_info = next_account_info(account_info_iter)?; // 4
|
||||
|
||||
let clock_info = next_account_info(account_info_iter)?; // 5
|
||||
let clock = Clock::from_account_info(clock_info)?;
|
||||
|
||||
let mut proposal_data = deserialize_proposal_raw(proposal_info)?;
|
||||
proposal_data.assert_can_edit_signatories()?;
|
||||
|
||||
let token_owner_record_data = deserialize_token_owner_record_for_proposal_owner(
|
||||
token_owner_record_info,
|
||||
&proposal_data.token_owner_record,
|
||||
)?;
|
||||
|
||||
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?;
|
||||
|
||||
let signatory_record_data =
|
||||
deserialize_signatory_record(signatory_record_info, proposal_info.key, &signatory)?;
|
||||
signatory_record_data.assert_can_remove_signatory()?;
|
||||
|
||||
proposal_data.signatories_count = proposal_data.signatories_count.checked_sub(1).unwrap();
|
||||
|
||||
// If all the remaining signatories signed already then we can start voting
|
||||
if proposal_data.signatories_count > 0
|
||||
&& proposal_data.signatories_signed_off_count == proposal_data.signatories_count
|
||||
{
|
||||
proposal_data.voting_at = Some(clock.slot);
|
||||
proposal_data.state = ProposalState::Voting;
|
||||
}
|
||||
|
||||
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||
|
||||
dispose_account(signatory_record_info, beneficiary_info);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
state::token_owner_record::deserialize_token_owner_record_raw,
|
||||
tools::asserts::assert_token_owner_or_delegate_is_signer,
|
||||
};
|
||||
|
||||
/// Processes SetGovernanceDelegate instruction
|
||||
pub fn process_set_governance_delegate(
|
||||
accounts: &[AccountInfo],
|
||||
new_governance_delegate: &Option<Pubkey>,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let governance_authority_info = next_account_info(account_info_iter)?; // 0
|
||||
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
|
||||
|
||||
let mut token_owner_record_data = deserialize_token_owner_record_raw(token_owner_record_info)?;
|
||||
|
||||
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, &governance_authority_info)?;
|
||||
|
||||
token_owner_record_data.governance_delegate = *new_governance_delegate;
|
||||
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
clock::Clock,
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
use crate::state::{
|
||||
enums::ProposalState, proposal::deserialize_proposal_raw,
|
||||
signatory_record::deserialize_signatory_record,
|
||||
};
|
||||
|
||||
/// Processes SignOffProposal instruction
|
||||
pub fn process_sign_off_proposal(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let proposal_info = next_account_info(account_info_iter)?; // 0
|
||||
|
||||
let signatory_record_info = next_account_info(account_info_iter)?; // 1
|
||||
let signatory_info = next_account_info(account_info_iter)?; // 2
|
||||
|
||||
let clock_info = next_account_info(account_info_iter)?; // 3
|
||||
let clock = Clock::from_account_info(clock_info)?;
|
||||
|
||||
let mut proposal_data = deserialize_proposal_raw(proposal_info)?;
|
||||
proposal_data.assert_can_sign_off()?;
|
||||
|
||||
let mut signatory_record_data =
|
||||
deserialize_signatory_record(signatory_record_info, proposal_info.key, signatory_info.key)?;
|
||||
signatory_record_data.assert_can_sign_off(signatory_info)?;
|
||||
|
||||
signatory_record_data.signed_off = true;
|
||||
signatory_record_data.serialize(&mut *signatory_record_info.data.borrow_mut())?;
|
||||
|
||||
if proposal_data.signatories_signed_off_count == 0 {
|
||||
proposal_data.signing_off_at = Some(clock.slot);
|
||||
proposal_data.state = ProposalState::SigningOff;
|
||||
}
|
||||
|
||||
proposal_data.signatories_signed_off_count = proposal_data
|
||||
.signatories_signed_off_count
|
||||
.checked_add(1)
|
||||
.unwrap();
|
||||
|
||||
// If all Signatories signed off we can start voting
|
||||
if proposal_data.signatories_signed_off_count == proposal_data.signatories_count {
|
||||
proposal_data.voting_at = Some(clock.slot);
|
||||
proposal_data.state = ProposalState::Voting;
|
||||
}
|
||||
|
||||
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
state::{
|
||||
realm::{deserialize_realm_raw, get_realm_address_seeds},
|
||||
token_owner_record::{
|
||||
deserialize_token_owner_record, get_token_owner_record_address_seeds,
|
||||
},
|
||||
},
|
||||
tools::token::{get_mint_from_token_account, transfer_spl_tokens_signed},
|
||||
};
|
||||
|
||||
/// Processes WithdrawGoverningTokens instruction
|
||||
pub fn process_withdraw_governing_tokens(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let realm_info = next_account_info(account_info_iter)?; // 0
|
||||
let governing_token_holding_info = next_account_info(account_info_iter)?; // 1
|
||||
let governing_token_destination_info = next_account_info(account_info_iter)?; // 2
|
||||
let governing_token_owner_info = next_account_info(account_info_iter)?; // 3
|
||||
let token_owner_record_info = next_account_info(account_info_iter)?; // 4
|
||||
let spl_token_info = next_account_info(account_info_iter)?; // 5
|
||||
|
||||
if !governing_token_owner_info.is_signer {
|
||||
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
||||
|
||||
let realm_data = deserialize_realm_raw(realm_info)?;
|
||||
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
|
||||
|
||||
let token_owner_record_address_seeds = get_token_owner_record_address_seeds(
|
||||
realm_info.key,
|
||||
&governing_token_mint,
|
||||
governing_token_owner_info.key,
|
||||
);
|
||||
|
||||
let mut token_owner_record_data =
|
||||
deserialize_token_owner_record(token_owner_record_info, &token_owner_record_address_seeds)?;
|
||||
|
||||
if token_owner_record_data.active_votes_count > 0 {
|
||||
return Err(GovernanceError::CannotWithdrawGoverningTokensWhenActiveVotesExist.into());
|
||||
}
|
||||
|
||||
transfer_spl_tokens_signed(
|
||||
&governing_token_holding_info,
|
||||
&governing_token_destination_info,
|
||||
&realm_info,
|
||||
&get_realm_address_seeds(&realm_data.name),
|
||||
program_id,
|
||||
token_owner_record_data.governing_token_deposit_amount,
|
||||
spl_token_info,
|
||||
)?;
|
||||
|
||||
token_owner_record_data.governing_token_deposit_amount = 0;
|
||||
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
//! State enumerations
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
|
||||
/// Defines all Governance accounts types
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum GovernanceAccountType {
|
||||
/// Default uninitialized account state
|
||||
Uninitialized,
|
||||
|
||||
/// Top level aggregation for governances with Community Token (and optional Council Token)
|
||||
Realm,
|
||||
|
||||
/// Token Owner Record for given governing token owner within a Realm
|
||||
TokenOwnerRecord,
|
||||
|
||||
/// Generic Account Governance account
|
||||
AccountGovernance,
|
||||
|
||||
/// Program Governance account
|
||||
ProgramGovernance,
|
||||
|
||||
/// Proposal account for Governance account. A single Governance account can have multiple Proposal accounts
|
||||
Proposal,
|
||||
|
||||
/// Proposal Signatory account
|
||||
SignatoryRecord,
|
||||
|
||||
/// Vote record account for a given Proposal. Proposal can have 0..n voting records
|
||||
ProposalVoteRecord,
|
||||
|
||||
/// Single Signer Instruction account which holds an instruction to execute for Proposal
|
||||
SingleSignerInstruction,
|
||||
}
|
||||
|
||||
impl Default for GovernanceAccountType {
|
||||
fn default() -> Self {
|
||||
GovernanceAccountType::Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
/// Vote with number of votes
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum VoteWeight {
|
||||
/// Yes vote
|
||||
Yes(u64),
|
||||
|
||||
/// No vote
|
||||
No(u64),
|
||||
}
|
||||
|
||||
/// What state a Proposal is in
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum ProposalState {
|
||||
/// Draft - Proposal enters Draft state when it's created
|
||||
Draft,
|
||||
|
||||
/// SigningOff - The Proposal is being signed off by Signatories
|
||||
/// Proposal enters the state when first Signatory Sings and leaves it when last Signatory signs
|
||||
SigningOff,
|
||||
|
||||
/// Taking votes
|
||||
Voting,
|
||||
|
||||
/// Voting ended with success
|
||||
Succeeded,
|
||||
|
||||
/// Voting completed and now instructions are being execute. Proposal enter this state when first instruction is executed and leaves when the last instruction is executed
|
||||
Executing,
|
||||
|
||||
/// Completed
|
||||
Completed,
|
||||
|
||||
/// Cancelled
|
||||
Cancelled,
|
||||
|
||||
/// Defeated
|
||||
Defeated,
|
||||
}
|
||||
|
||||
impl Default for ProposalState {
|
||||
fn default() -> Self {
|
||||
ProposalState::Draft
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
//! Governance Account
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError, id, state::enums::GovernanceAccountType,
|
||||
tools::account::deserialize_account, tools::account::AccountMaxSize,
|
||||
};
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use super::realm::assert_is_valid_realm;
|
||||
|
||||
/// Governance config
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct GovernanceConfig {
|
||||
/// Governance Realm
|
||||
pub realm: Pubkey,
|
||||
|
||||
/// Account governed by this Governance. It can be for example Program account, Mint account or Token Account
|
||||
pub governed_account: Pubkey,
|
||||
|
||||
/// Voting threshold in % required to tip the vote
|
||||
/// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote
|
||||
pub vote_threshold_percentage: u8,
|
||||
|
||||
/// Minimum number of tokens a governance token owner must possess to be able to create a proposal
|
||||
pub min_tokens_to_create_proposal: u16,
|
||||
|
||||
/// Minimum waiting time in slots for an instruction to be executed after proposal is voted on
|
||||
pub min_instruction_hold_up_time: u64,
|
||||
|
||||
/// Time limit in slots for proposal to be open for voting
|
||||
pub max_voting_time: u64,
|
||||
}
|
||||
|
||||
/// Governance Account
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct Governance {
|
||||
/// Account type. It can be Uninitialized, AccountGovernance or ProgramGovernance
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
||||
/// Governance config
|
||||
pub config: GovernanceConfig,
|
||||
|
||||
/// Running count of proposals
|
||||
pub proposals_count: u16,
|
||||
}
|
||||
|
||||
impl AccountMaxSize for Governance {}
|
||||
|
||||
impl IsInitialized for Governance {
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.account_type == GovernanceAccountType::AccountGovernance
|
||||
|| self.account_type == GovernanceAccountType::ProgramGovernance
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes account and checks owner program
|
||||
pub fn deserialize_governance_raw(
|
||||
governance_info: &AccountInfo,
|
||||
) -> Result<Governance, ProgramError> {
|
||||
deserialize_account::<Governance>(governance_info, &id())
|
||||
}
|
||||
|
||||
/// Returns ProgramGovernance PDA seeds
|
||||
pub fn get_program_governance_address_seeds<'a>(
|
||||
realm: &'a Pubkey,
|
||||
governed_program: &'a Pubkey,
|
||||
) -> [&'a [u8]; 3] {
|
||||
// 'program-governance' prefix ensures uniqueness of the PDA
|
||||
// Note: Only the current program upgrade authority can create an account with this PDA using CreateProgramGovernance instruction
|
||||
[
|
||||
b"program-governance",
|
||||
&realm.as_ref(),
|
||||
&governed_program.as_ref(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns ProgramGovernance PDA address
|
||||
pub fn get_program_governance_address<'a>(
|
||||
realm: &'a Pubkey,
|
||||
governed_program: &'a Pubkey,
|
||||
) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&get_program_governance_address_seeds(realm, governed_program),
|
||||
&id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
/// Returns AccountGovernance PDA seeds
|
||||
pub fn get_account_governance_address_seeds<'a>(
|
||||
realm: &'a Pubkey,
|
||||
governed_account: &'a Pubkey,
|
||||
) -> [&'a [u8]; 3] {
|
||||
[
|
||||
b"account-governance",
|
||||
&realm.as_ref(),
|
||||
&governed_account.as_ref(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns AccountGovernance PDA address
|
||||
pub fn get_account_governance_address<'a>(
|
||||
realm: &'a Pubkey,
|
||||
governed_account: &'a Pubkey,
|
||||
) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&get_account_governance_address_seeds(realm, governed_account),
|
||||
&id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
/// Validates governance config
|
||||
pub fn assert_is_valid_governance_config(
|
||||
governance_config: &GovernanceConfig,
|
||||
realm_info: &AccountInfo,
|
||||
) -> Result<(), ProgramError> {
|
||||
if realm_info.key != &governance_config.realm {
|
||||
return Err(GovernanceError::InvalidGovernanceConfig.into());
|
||||
}
|
||||
|
||||
assert_is_valid_realm(realm_info)?;
|
||||
|
||||
if governance_config.vote_threshold_percentage < 50
|
||||
|| governance_config.vote_threshold_percentage > 100
|
||||
{
|
||||
return Err(GovernanceError::InvalidGovernanceConfig.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
//! Program accounts
|
||||
|
||||
pub mod enums;
|
||||
pub mod governance;
|
||||
pub mod proposal;
|
||||
pub mod proposal_vote_record;
|
||||
pub mod realm;
|
||||
pub mod signatory_record;
|
||||
pub mod single_signer_instruction;
|
||||
pub mod token_owner_record;
|
|
@ -1,254 +0,0 @@
|
|||
//! Proposal Account
|
||||
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, epoch_schedule::Slot, program_error::ProgramError,
|
||||
program_pack::IsInitialized, pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
id,
|
||||
tools::account::{deserialize_account, AccountMaxSize},
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
};
|
||||
|
||||
use crate::state::enums::{GovernanceAccountType, ProposalState};
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
|
||||
/// Governance Proposal
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct Proposal {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
||||
/// Governance account the Proposal belongs to
|
||||
pub governance: Pubkey,
|
||||
|
||||
/// Indicates which Governing Token is used to vote on the Proposal
|
||||
/// Whether the general Community token owners or the Council tokens owners vote on this Proposal
|
||||
pub governing_token_mint: Pubkey,
|
||||
|
||||
/// Current proposal state
|
||||
pub state: ProposalState,
|
||||
|
||||
/// The TokenOwnerRecord representing the user who created and owns this Proposal
|
||||
pub token_owner_record: Pubkey,
|
||||
|
||||
/// The number of signatories assigned to the Proposal
|
||||
pub signatories_count: u8,
|
||||
|
||||
/// The number of signatories who already signed
|
||||
pub signatories_signed_off_count: u8,
|
||||
|
||||
/// Link to proposal's description
|
||||
pub description_link: String,
|
||||
|
||||
/// Proposal name
|
||||
pub name: String,
|
||||
|
||||
/// When the Proposal was created and entered Draft state
|
||||
pub draft_at: Slot,
|
||||
|
||||
/// When Signatories started signing off the Proposal
|
||||
pub signing_off_at: Option<Slot>,
|
||||
|
||||
/// When the Proposal began voting
|
||||
pub voting_at: Option<Slot>,
|
||||
|
||||
/// When the Proposal ended voting and entered either Succeeded or Defeated
|
||||
pub voting_completed_at: Option<Slot>,
|
||||
|
||||
/// When the Proposal entered Executing state
|
||||
pub executing_at: Option<Slot>,
|
||||
|
||||
/// When the Proposal entered final state Completed or Cancelled and was closed
|
||||
pub closed_at: Option<Slot>,
|
||||
|
||||
/// The number of the instructions already executed
|
||||
pub number_of_executed_instructions: u8,
|
||||
|
||||
/// The number of instructions included in the proposal
|
||||
pub number_of_instructions: u8,
|
||||
}
|
||||
|
||||
impl AccountMaxSize for Proposal {
|
||||
fn get_max_size(&self) -> Option<usize> {
|
||||
Some(self.name.len() + self.description_link.len() + 163)
|
||||
}
|
||||
}
|
||||
|
||||
impl IsInitialized for Proposal {
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.account_type == GovernanceAccountType::Proposal
|
||||
}
|
||||
}
|
||||
|
||||
impl Proposal {
|
||||
/// Checks if Signatories can be edited (added or removed) for the Proposal in the given state
|
||||
pub fn assert_can_edit_signatories(&self) -> Result<(), ProgramError> {
|
||||
if !(self.state == ProposalState::Draft || self.state == ProposalState::SigningOff) {
|
||||
return Err(GovernanceError::InvalidStateCannotEditSignatories.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if Proposal can be singed off
|
||||
pub fn assert_can_sign_off(&self) -> Result<(), ProgramError> {
|
||||
if !(self.state == ProposalState::Draft || self.state == ProposalState::SigningOff) {
|
||||
return Err(GovernanceError::InvalidStateCannotSignOff.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes Proposal account and checks owner program
|
||||
pub fn deserialize_proposal_raw(proposal_info: &AccountInfo) -> Result<Proposal, ProgramError> {
|
||||
deserialize_account::<Proposal>(proposal_info, &id())
|
||||
}
|
||||
|
||||
/// Returns Proposal PDA seeds
|
||||
pub fn get_proposal_address_seeds<'a>(
|
||||
governance: &'a Pubkey,
|
||||
governing_token_mint: &'a Pubkey,
|
||||
proposal_index_le_bytes: &'a [u8],
|
||||
) -> [&'a [u8]; 4] {
|
||||
[
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
governance.as_ref(),
|
||||
governing_token_mint.as_ref(),
|
||||
&proposal_index_le_bytes,
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns Proposal PDA address
|
||||
pub fn get_proposal_address<'a>(
|
||||
governance: &'a Pubkey,
|
||||
governing_token_mint: &'a Pubkey,
|
||||
proposal_index_bytes: &'a [u8],
|
||||
) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&get_proposal_address_seeds(governance, governing_token_mint, &proposal_index_bytes),
|
||||
&id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use {super::*, proptest::prelude::*};
|
||||
|
||||
fn create_test_proposal() -> Proposal {
|
||||
Proposal {
|
||||
account_type: GovernanceAccountType::TokenOwnerRecord,
|
||||
governance: Pubkey::new_unique(),
|
||||
governing_token_mint: Pubkey::new_unique(),
|
||||
state: ProposalState::Draft,
|
||||
token_owner_record: Pubkey::new_unique(),
|
||||
signatories_count: 10,
|
||||
signatories_signed_off_count: 5,
|
||||
description_link: "This is my description".to_string(),
|
||||
name: "This is my name".to_string(),
|
||||
draft_at: 10,
|
||||
signing_off_at: Some(10),
|
||||
voting_at: Some(10),
|
||||
voting_completed_at: Some(10),
|
||||
executing_at: Some(10),
|
||||
closed_at: Some(10),
|
||||
number_of_executed_instructions: 10,
|
||||
number_of_instructions: 10,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_size() {
|
||||
let proposal = create_test_proposal();
|
||||
let size = proposal.try_to_vec().unwrap().len();
|
||||
|
||||
assert_eq!(proposal.get_max_size(), Some(size));
|
||||
}
|
||||
|
||||
fn editable_signatory_states() -> impl Strategy<Value = ProposalState> {
|
||||
prop_oneof![Just(ProposalState::Draft), Just(ProposalState::SigningOff),]
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_assert_can_edit_signatories(state in editable_signatory_states()) {
|
||||
|
||||
let mut proposal = create_test_proposal();
|
||||
proposal.state = state;
|
||||
proposal.assert_can_edit_signatories().unwrap();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn none_editable_signatory_states() -> impl Strategy<Value = ProposalState> {
|
||||
prop_oneof![
|
||||
Just(ProposalState::Voting),
|
||||
Just(ProposalState::Succeeded),
|
||||
Just(ProposalState::Executing),
|
||||
Just(ProposalState::Completed),
|
||||
Just(ProposalState::Cancelled),
|
||||
Just(ProposalState::Defeated),
|
||||
]
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_assert_can_edit_signatories_with_invalid_state_error(state in none_editable_signatory_states()) {
|
||||
// Arrange
|
||||
let mut proposal = create_test_proposal();
|
||||
proposal.state = state;
|
||||
|
||||
// Act
|
||||
let err = proposal.assert_can_edit_signatories().err().unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, GovernanceError::InvalidStateCannotEditSignatories.into());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn sign_off_states() -> impl Strategy<Value = ProposalState> {
|
||||
prop_oneof![Just(ProposalState::SigningOff), Just(ProposalState::Draft),]
|
||||
}
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_assert_can_sign_off(state in sign_off_states()) {
|
||||
let mut proposal = create_test_proposal();
|
||||
proposal.state = state;
|
||||
proposal.assert_can_sign_off().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn none_sign_off_states() -> impl Strategy<Value = ProposalState> {
|
||||
prop_oneof![
|
||||
Just(ProposalState::Voting),
|
||||
Just(ProposalState::Succeeded),
|
||||
Just(ProposalState::Executing),
|
||||
Just(ProposalState::Completed),
|
||||
Just(ProposalState::Cancelled),
|
||||
Just(ProposalState::Defeated),
|
||||
]
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_assert_can_sign_off_with_state_error(state in none_sign_off_states()) {
|
||||
// Arrange
|
||||
let mut proposal = create_test_proposal();
|
||||
proposal.state = state;
|
||||
|
||||
// Act
|
||||
let err = proposal.assert_can_sign_off().err().unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, GovernanceError::InvalidStateCannotSignOff.into());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
//! Proposal Vote Record Account
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
use crate::state::enums::{GovernanceAccountType, VoteWeight};
|
||||
|
||||
/// Proposal Vote Record
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct ProposalVoteRecord {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
||||
/// Proposal account
|
||||
pub proposal: Pubkey,
|
||||
|
||||
/// The user who casted this vote
|
||||
/// This is the Governing Token Owner who deposited governing tokens into the Realm
|
||||
pub governing_token_owner: Pubkey,
|
||||
|
||||
/// Voter's vote: Yes/No and amount
|
||||
pub vote: Option<VoteWeight>,
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
//! Realm Account
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
id,
|
||||
tools::account::{assert_is_valid_account, deserialize_account, AccountMaxSize},
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
};
|
||||
|
||||
use crate::state::enums::GovernanceAccountType;
|
||||
|
||||
/// Governance Realm Account
|
||||
/// Account PDA seeds" ['governance', name]
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct Realm {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
||||
/// Community mint
|
||||
pub community_mint: Pubkey,
|
||||
|
||||
/// Council mint
|
||||
pub council_mint: Option<Pubkey>,
|
||||
|
||||
/// Governance Realm name
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl AccountMaxSize for Realm {}
|
||||
|
||||
impl IsInitialized for Realm {
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.account_type == GovernanceAccountType::Realm
|
||||
}
|
||||
}
|
||||
|
||||
impl Realm {
|
||||
/// Asserts the given mint is either Community or Council mint of the Realm
|
||||
pub fn assert_is_valid_governing_token_mint(
|
||||
&self,
|
||||
governing_token_mint: &Pubkey,
|
||||
) -> Result<(), ProgramError> {
|
||||
if self.community_mint == *governing_token_mint {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.council_mint == Some(*governing_token_mint) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(GovernanceError::InvalidGoverningTokenMint.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether realm account exists, is initialized and owned by Governance program
|
||||
pub fn assert_is_valid_realm(realm_info: &AccountInfo) -> Result<(), ProgramError> {
|
||||
assert_is_valid_account(realm_info, GovernanceAccountType::Realm, &id())
|
||||
}
|
||||
|
||||
/// Deserializes account and checks owner program
|
||||
pub fn deserialize_realm_raw(realm_info: &AccountInfo) -> Result<Realm, ProgramError> {
|
||||
deserialize_account::<Realm>(realm_info, &id())
|
||||
}
|
||||
|
||||
/// Returns Realm PDA seeds
|
||||
pub fn get_realm_address_seeds(name: &str) -> [&[u8]; 2] {
|
||||
[PROGRAM_AUTHORITY_SEED, &name.as_bytes()]
|
||||
}
|
||||
|
||||
/// Returns Realm PDA address
|
||||
pub fn get_realm_address(name: &str) -> Pubkey {
|
||||
Pubkey::find_program_address(&get_realm_address_seeds(&name), &id()).0
|
||||
}
|
||||
|
||||
/// Returns Realm Token Holding PDA seeds
|
||||
pub fn get_governing_token_holding_address_seeds<'a>(
|
||||
realm: &'a Pubkey,
|
||||
governing_token_mint: &'a Pubkey,
|
||||
) -> [&'a [u8]; 3] {
|
||||
[
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
realm.as_ref(),
|
||||
governing_token_mint.as_ref(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns Realm Token Holding PDA address
|
||||
pub fn get_governing_token_holding_address(
|
||||
realm: &Pubkey,
|
||||
governing_token_mint: &Pubkey,
|
||||
) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&get_governing_token_holding_address_seeds(realm, governing_token_mint),
|
||||
&id(),
|
||||
)
|
||||
.0
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
//! Signatory Record
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
id,
|
||||
tools::account::{deserialize_account, AccountMaxSize},
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
};
|
||||
|
||||
use crate::state::enums::GovernanceAccountType;
|
||||
|
||||
/// Account PDA seeds: ['governance', proposal, signatory]
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct SignatoryRecord {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
/// Proposal the signatory is assigned for
|
||||
pub proposal: Pubkey,
|
||||
/// The account of the signatory who can sign off the proposal
|
||||
pub signatory: Pubkey,
|
||||
/// Indicates whether the signatory signed off the proposal
|
||||
pub signed_off: bool,
|
||||
}
|
||||
|
||||
impl AccountMaxSize for SignatoryRecord {}
|
||||
|
||||
impl IsInitialized for SignatoryRecord {
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.account_type == GovernanceAccountType::SignatoryRecord
|
||||
}
|
||||
}
|
||||
|
||||
impl SignatoryRecord {
|
||||
/// Checks signatory hasn't signed off yet and is transaction signer
|
||||
pub fn assert_can_sign_off(&self, signatory_info: &AccountInfo) -> Result<(), ProgramError> {
|
||||
if self.signed_off {
|
||||
return Err(GovernanceError::SignatoryAlreadySignedOff.into());
|
||||
}
|
||||
|
||||
if !signatory_info.is_signer {
|
||||
return Err(GovernanceError::SignatoryMustSign.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks signatory can be removed from Proposal
|
||||
pub fn assert_can_remove_signatory(&self) -> Result<(), ProgramError> {
|
||||
if self.signed_off {
|
||||
return Err(GovernanceError::SignatoryAlreadySignedOff.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns SignatoryRecord PDA seeds
|
||||
pub fn get_signatory_record_address_seeds<'a>(
|
||||
proposal: &'a Pubkey,
|
||||
signatory: &'a Pubkey,
|
||||
) -> [&'a [u8]; 3] {
|
||||
[
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
proposal.as_ref(),
|
||||
signatory.as_ref(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns SignatoryRecord PDA address
|
||||
pub fn get_signatory_record_address<'a>(proposal: &'a Pubkey, signatory: &'a Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&get_signatory_record_address_seeds(proposal, signatory),
|
||||
&id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
/// Deserializes SignatoryRecord account and checks owner program
|
||||
pub fn deserialize_signatory_record_raw(
|
||||
signatory_record_info: &AccountInfo,
|
||||
) -> Result<SignatoryRecord, ProgramError> {
|
||||
deserialize_account::<SignatoryRecord>(signatory_record_info, &id())
|
||||
}
|
||||
|
||||
/// Deserializes SignatoryRecord and validates its PDA
|
||||
pub fn deserialize_signatory_record(
|
||||
signatory_record_info: &AccountInfo,
|
||||
proposal: &Pubkey,
|
||||
signatory: &Pubkey,
|
||||
) -> Result<SignatoryRecord, ProgramError> {
|
||||
let (signatory_record_address, _) = Pubkey::find_program_address(
|
||||
&get_signatory_record_address_seeds(proposal, signatory),
|
||||
&id(),
|
||||
);
|
||||
|
||||
if signatory_record_address != *signatory_record_info.key {
|
||||
return Err(GovernanceError::InvalidSignatoryAddress.into());
|
||||
}
|
||||
|
||||
deserialize_signatory_record_raw(signatory_record_info)
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
//! SingleSignerInstruction Account
|
||||
|
||||
use crate::state::enums::GovernanceAccountType;
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
|
||||
/// Account for an instruction to be executed for Proposal
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct SingleSignerInstruction {
|
||||
/// Governance Account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
||||
/// Minimum waiting time in slots for the instruction to be executed once proposal is voted on
|
||||
pub hold_up_time: u64,
|
||||
|
||||
/// Instruction to execute
|
||||
/// The instruction will be signed by Governance PDA the Proposal belongs to
|
||||
// For example for ProgramGovernance the instruction to upgrade program will be signed by ProgramGovernance PDA
|
||||
pub instruction: InstructionData,
|
||||
|
||||
/// Executed flag
|
||||
pub executed: bool,
|
||||
}
|
||||
|
||||
/// Temp. placeholder until I get Borsh serialization for Instruction working
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
#[repr(C)]
|
||||
pub struct InstructionData {}
|
|
@ -1,166 +0,0 @@
|
|||
//! Token Owner Record Account
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
id,
|
||||
tools::account::{deserialize_account, AccountMaxSize},
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
};
|
||||
|
||||
use crate::state::enums::GovernanceAccountType;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
/// Governance Token Owner Record
|
||||
/// Account PDA seeds: ['governance', realm, token_mint, token_owner ]
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct TokenOwnerRecord {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
||||
/// The Realm the TokenOwnerRecord belongs to
|
||||
pub realm: Pubkey,
|
||||
|
||||
/// Governing Token Mint the TokenOwnerRecord holds deposit for
|
||||
pub governing_token_mint: Pubkey,
|
||||
|
||||
/// The owner (either single or multisig) of the deposited governing SPL Tokens
|
||||
/// This is who can authorize a withdrawal
|
||||
pub governing_token_owner: Pubkey,
|
||||
|
||||
/// The amount of governing tokens deposited into the Realm
|
||||
/// This amount is the voter weight used when voting on proposals
|
||||
pub governing_token_deposit_amount: u64,
|
||||
|
||||
/// A single account that is allowed to operate governance with the deposited governing tokens
|
||||
/// It's delegated to by the governing token owner or current governance_delegate
|
||||
pub governance_delegate: Option<Pubkey>,
|
||||
|
||||
/// The number of active votes cast by TokenOwner
|
||||
pub active_votes_count: u16,
|
||||
|
||||
/// The total number of votes cast by the TokenOwner
|
||||
pub total_votes_count: u16,
|
||||
}
|
||||
|
||||
impl AccountMaxSize for TokenOwnerRecord {
|
||||
fn get_max_size(&self) -> Option<usize> {
|
||||
Some(142)
|
||||
}
|
||||
}
|
||||
|
||||
impl IsInitialized for TokenOwnerRecord {
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.account_type == GovernanceAccountType::TokenOwnerRecord
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns TokenOwnerRecord PDA address
|
||||
pub fn get_token_owner_record_address(
|
||||
realm: &Pubkey,
|
||||
governing_token_mint: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&get_token_owner_record_address_seeds(realm, governing_token_mint, governing_token_owner),
|
||||
&id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
/// Returns TokenOwnerRecord PDA seeds
|
||||
pub fn get_token_owner_record_address_seeds<'a>(
|
||||
realm: &'a Pubkey,
|
||||
governing_token_mint: &'a Pubkey,
|
||||
governing_token_owner: &'a Pubkey,
|
||||
) -> [&'a [u8]; 4] {
|
||||
[
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
realm.as_ref(),
|
||||
governing_token_mint.as_ref(),
|
||||
governing_token_owner.as_ref(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Deserializes TokenOwnerRecord account and checks owner program
|
||||
pub fn deserialize_token_owner_record_raw(
|
||||
token_owner_record_info: &AccountInfo,
|
||||
) -> Result<TokenOwnerRecord, ProgramError> {
|
||||
deserialize_account::<TokenOwnerRecord>(token_owner_record_info, &id())
|
||||
}
|
||||
|
||||
/// Deserializes TokenOwnerRecord account and checks its PDA against the provided seeds
|
||||
pub fn deserialize_token_owner_record(
|
||||
token_owner_record_info: &AccountInfo,
|
||||
token_owner_record_seeds: &[&[u8]],
|
||||
) -> Result<TokenOwnerRecord, ProgramError> {
|
||||
let (token_owner_record_address, _) =
|
||||
Pubkey::find_program_address(token_owner_record_seeds, &id());
|
||||
|
||||
if token_owner_record_address != *token_owner_record_info.key {
|
||||
return Err(GovernanceError::InvalidTokenOwnerRecordAccountAddress.into());
|
||||
}
|
||||
|
||||
deserialize_token_owner_record_raw(token_owner_record_info)
|
||||
}
|
||||
|
||||
/// Deserializes TokenOwnerRecord account and checks that its PDA matches the given realm and governing mint
|
||||
pub fn deserialize_token_owner_record_for_realm_and_governing_mint(
|
||||
token_owner_record_info: &AccountInfo,
|
||||
realm: &Pubkey,
|
||||
governing_token_mint: &Pubkey,
|
||||
) -> Result<TokenOwnerRecord, ProgramError> {
|
||||
let token_owner_record_data = deserialize_token_owner_record_raw(token_owner_record_info)?;
|
||||
|
||||
if token_owner_record_data.governing_token_mint != *governing_token_mint {
|
||||
return Err(GovernanceError::InvalidTokenOwnerRecordGoverningMint.into());
|
||||
}
|
||||
|
||||
if token_owner_record_data.realm != *realm {
|
||||
return Err(GovernanceError::InvalidTokenOwnerRecordRealm.into());
|
||||
}
|
||||
|
||||
Ok(token_owner_record_data)
|
||||
}
|
||||
|
||||
/// Deserializes TokenOwnerRecord account and checks its address is the give proposal_owner
|
||||
pub fn deserialize_token_owner_record_for_proposal_owner(
|
||||
token_owner_record_info: &AccountInfo,
|
||||
proposal_owner: &Pubkey,
|
||||
) -> Result<TokenOwnerRecord, ProgramError> {
|
||||
if token_owner_record_info.key != proposal_owner {
|
||||
return Err(GovernanceError::InvalidProposalOwnerAccount.into());
|
||||
}
|
||||
|
||||
deserialize_token_owner_record_raw(token_owner_record_info)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use solana_program::borsh::get_packed_len;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_max_size() {
|
||||
let token_owner_record = TokenOwnerRecord {
|
||||
account_type: GovernanceAccountType::TokenOwnerRecord,
|
||||
realm: Pubkey::new_unique(),
|
||||
governing_token_mint: Pubkey::new_unique(),
|
||||
governing_token_owner: Pubkey::new_unique(),
|
||||
governing_token_deposit_amount: 10,
|
||||
governance_delegate: Some(Pubkey::new_unique()),
|
||||
active_votes_count: 1,
|
||||
total_votes_count: 1,
|
||||
};
|
||||
|
||||
let size = get_packed_len::<TokenOwnerRecord>();
|
||||
|
||||
assert_eq!(token_owner_record.get_max_size(), Some(size));
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
//! Proposal Vote Record Account
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{program_pack::IsInitialized, pubkey::Pubkey};
|
||||
|
||||
use crate::{id, tools::account::AccountMaxSize, PROGRAM_AUTHORITY_SEED};
|
||||
|
||||
use crate::state::enums::{GovernanceAccountType, VoteWeight};
|
||||
|
||||
/// Proposal VoteRecord
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct VoteRecord {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
||||
/// Proposal account
|
||||
pub proposal: Pubkey,
|
||||
|
||||
/// The user who casted this vote
|
||||
/// This is the Governing Token Owner who deposited governing tokens into the Realm
|
||||
pub governing_token_owner: Pubkey,
|
||||
|
||||
/// Voter's vote: Yes/No and amount
|
||||
pub vote_weight: VoteWeight,
|
||||
}
|
||||
|
||||
impl AccountMaxSize for VoteRecord {}
|
||||
|
||||
impl IsInitialized for VoteRecord {
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.account_type == GovernanceAccountType::VoteRecord
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns VoteRecord PDA seeds
|
||||
pub fn get_vote_record_address_seeds<'a>(
|
||||
proposal: &'a Pubkey,
|
||||
token_owner_record: &'a Pubkey,
|
||||
) -> [&'a [u8]; 3] {
|
||||
[
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
proposal.as_ref(),
|
||||
token_owner_record.as_ref(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns VoteRecord PDA address
|
||||
pub fn get_vote_record_address<'a>(proposal: &'a Pubkey, token_owner_record: &'a Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&get_vote_record_address_seeds(proposal, token_owner_record),
|
||||
&id(),
|
||||
)
|
||||
.0
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
//! General purpose account utility functions
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, borsh::try_from_slice_unchecked, msg, program::invoke_signed,
|
||||
program_error::ProgramError, program_pack::IsInitialized, pubkey::Pubkey, rent::Rent,
|
||||
system_instruction::create_account,
|
||||
};
|
||||
|
||||
use crate::error::GovernanceError;
|
||||
|
||||
/// Trait for accounts to return their max size
|
||||
pub trait AccountMaxSize {
|
||||
/// Returns max account size or None if max size is not known and actual instance size should be used
|
||||
fn get_max_size(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new account and serializes data into it using the provided seeds to invoke signed CPI call
|
||||
/// Note: This functions also checks the provided account PDA matches the supplied seeds
|
||||
pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSize>(
|
||||
payer_info: &AccountInfo<'a>,
|
||||
account_info: &AccountInfo<'a>,
|
||||
account_data: &T,
|
||||
account_address_seeds: &[&[u8]],
|
||||
program_id: &Pubkey,
|
||||
system_info: &AccountInfo<'a>,
|
||||
rent: &Rent,
|
||||
) -> Result<(), ProgramError> {
|
||||
// Get PDA and assert it's the same as the requested account address
|
||||
let (account_address, bump_seed) =
|
||||
Pubkey::find_program_address(account_address_seeds, program_id);
|
||||
|
||||
if account_address != *account_info.key {
|
||||
msg!(
|
||||
"Create account with PDA: {:?} was requested while PDA: {:?} was expected",
|
||||
account_info.key,
|
||||
account_address
|
||||
);
|
||||
return Err(ProgramError::InvalidSeeds);
|
||||
}
|
||||
|
||||
let (serialized_data, account_size) = if let Some(max_size) = account_data.get_max_size() {
|
||||
(None, max_size)
|
||||
} else {
|
||||
let serialized_data = account_data.try_to_vec()?;
|
||||
let account_size = serialized_data.len();
|
||||
(Some(serialized_data), account_size)
|
||||
};
|
||||
|
||||
let create_account_instruction = create_account(
|
||||
payer_info.key,
|
||||
account_info.key,
|
||||
rent.minimum_balance(account_size),
|
||||
account_size as u64,
|
||||
program_id,
|
||||
);
|
||||
|
||||
let mut signers_seeds = account_address_seeds.to_vec();
|
||||
let bump = &[bump_seed];
|
||||
signers_seeds.push(bump);
|
||||
|
||||
invoke_signed(
|
||||
&create_account_instruction,
|
||||
&[
|
||||
payer_info.clone(),
|
||||
account_info.clone(),
|
||||
system_info.clone(),
|
||||
],
|
||||
&[&signers_seeds[..]],
|
||||
)?;
|
||||
|
||||
if let Some(serialized_data) = serialized_data {
|
||||
account_info
|
||||
.data
|
||||
.borrow_mut()
|
||||
.copy_from_slice(&serialized_data);
|
||||
} else {
|
||||
account_data.serialize(&mut *account_info.data.borrow_mut())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deserializes account and checks it's initialized and owned by the specified program
|
||||
pub fn deserialize_account<T: BorshDeserialize + IsInitialized>(
|
||||
account_info: &AccountInfo,
|
||||
owner_program_id: &Pubkey,
|
||||
) -> Result<T, ProgramError> {
|
||||
if account_info.data_is_empty() {
|
||||
return Err(ProgramError::UninitializedAccount);
|
||||
}
|
||||
if account_info.owner != owner_program_id {
|
||||
return Err(GovernanceError::InvalidAccountOwner.into());
|
||||
}
|
||||
|
||||
let account: T = try_from_slice_unchecked(&account_info.data.borrow())?;
|
||||
if !account.is_initialized() {
|
||||
Err(ProgramError::UninitializedAccount)
|
||||
} else {
|
||||
Ok(account)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts the given account is not empty, owned given program and of the expected type
|
||||
pub fn assert_is_valid_account<T: BorshDeserialize + PartialEq>(
|
||||
account_info: &AccountInfo,
|
||||
expected_account_type: T,
|
||||
owner_program_id: &Pubkey,
|
||||
) -> Result<(), ProgramError> {
|
||||
if account_info.owner != owner_program_id {
|
||||
return Err(GovernanceError::InvalidAccountOwner.into());
|
||||
}
|
||||
|
||||
if account_info.data_is_empty() {
|
||||
return Err(ProgramError::UninitializedAccount);
|
||||
}
|
||||
|
||||
let account_type: T = try_from_slice_unchecked(&account_info.data.borrow())?;
|
||||
|
||||
if account_type != expected_account_type {
|
||||
return Err(GovernanceError::InvalidAccountType.into());
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disposes account by transferring its lamports to the beneficiary account and zeros its data
|
||||
// After transaction completes the runtime would remove the account with no lamports
|
||||
pub fn dispose_account(account_info: &AccountInfo, beneficiary_account: &AccountInfo) {
|
||||
let account_lamports = account_info.lamports();
|
||||
**account_info.lamports.borrow_mut() = 0;
|
||||
|
||||
**beneficiary_account.lamports.borrow_mut() = beneficiary_account
|
||||
.lamports()
|
||||
.checked_add(account_lamports)
|
||||
.unwrap();
|
||||
|
||||
let mut account_data = account_info.data.borrow_mut();
|
||||
|
||||
account_data.fill(0);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
//! Governance asserts
|
||||
|
||||
use solana_program::{account_info::AccountInfo, program_error::ProgramError};
|
||||
|
||||
use crate::{error::GovernanceError, state::token_owner_record::TokenOwnerRecord};
|
||||
|
||||
/// Checks whether the provided Governance Authority signed transaction
|
||||
pub fn assert_token_owner_or_delegate_is_signer(
|
||||
token_owner_record: &TokenOwnerRecord,
|
||||
governance_authority_info: &AccountInfo,
|
||||
) -> Result<(), ProgramError> {
|
||||
if governance_authority_info.is_signer {
|
||||
if &token_owner_record.governing_token_owner == governance_authority_info.key {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(governance_delegate) = token_owner_record.governance_delegate {
|
||||
if &governance_delegate == governance_authority_info.key {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Err(GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into())
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
//! General purpose bpf_loader_upgradeable utility functions
|
||||
|
||||
use solana_program::{
|
||||
account_info::AccountInfo,
|
||||
bpf_loader_upgradeable::{self, UpgradeableLoaderState},
|
||||
program::invoke,
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use bincode::deserialize;
|
||||
|
||||
use crate::error::GovernanceError;
|
||||
|
||||
/// Returns ProgramData account address for the given Program
|
||||
pub fn get_program_data_address(program: &Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(&[program.as_ref()], &bpf_loader_upgradeable::id()).0
|
||||
}
|
||||
|
||||
/// Returns upgrade_authority from the given Upgradable Loader Account
|
||||
pub fn get_program_upgrade_authority(
|
||||
upgradable_loader_state: &UpgradeableLoaderState,
|
||||
) -> Result<Option<Pubkey>, ProgramError> {
|
||||
let upgrade_authority = match upgradable_loader_state {
|
||||
UpgradeableLoaderState::ProgramData {
|
||||
slot: _,
|
||||
upgrade_authority_address,
|
||||
} => *upgrade_authority_address,
|
||||
_ => return Err(ProgramError::InvalidAccountData),
|
||||
};
|
||||
|
||||
Ok(upgrade_authority)
|
||||
}
|
||||
|
||||
/// Sets new upgrade authority for the given upgradable program
|
||||
pub fn set_program_upgrade_authority<'a>(
|
||||
program_address: &Pubkey,
|
||||
program_data_info: &AccountInfo<'a>,
|
||||
program_upgrade_authority_info: &AccountInfo<'a>,
|
||||
new_authority_info: &AccountInfo<'a>,
|
||||
bpf_upgrade_loader_info: &AccountInfo<'a>,
|
||||
) -> Result<(), ProgramError> {
|
||||
let set_upgrade_authority_instruction = bpf_loader_upgradeable::set_upgrade_authority(
|
||||
program_address,
|
||||
&program_upgrade_authority_info.key,
|
||||
Some(&new_authority_info.key),
|
||||
);
|
||||
|
||||
invoke(
|
||||
&set_upgrade_authority_instruction,
|
||||
&[
|
||||
program_data_info.clone(),
|
||||
program_upgrade_authority_info.clone(),
|
||||
bpf_upgrade_loader_info.clone(),
|
||||
new_authority_info.clone(),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Asserts the program is upgradable and its upgrade authority is a signer of the transaction
|
||||
pub fn assert_program_upgrade_authority_is_signer(
|
||||
program_address: &Pubkey,
|
||||
program_data_info: &AccountInfo,
|
||||
program_upgrade_authority_info: &AccountInfo,
|
||||
) -> Result<(), ProgramError> {
|
||||
if program_data_info.owner != &bpf_loader_upgradeable::id() {
|
||||
return Err(ProgramError::IncorrectProgramId);
|
||||
}
|
||||
let program_data_address = get_program_data_address(program_address);
|
||||
|
||||
if program_data_address != *program_data_info.key {
|
||||
return Err(GovernanceError::InvalidProgramDataAccountAddress.into());
|
||||
}
|
||||
|
||||
let upgrade_authority = if let UpgradeableLoaderState::ProgramData {
|
||||
slot: _,
|
||||
upgrade_authority_address,
|
||||
} = deserialize(&program_data_info.data.borrow())
|
||||
.map_err(|_| GovernanceError::InvalidProgramDataAccountData)?
|
||||
{
|
||||
upgrade_authority_address
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let upgrade_authority = upgrade_authority.ok_or(GovernanceError::ProgramNotUpgradable)?;
|
||||
|
||||
if upgrade_authority != *program_upgrade_authority_info.key {
|
||||
return Err(GovernanceError::InvalidUpgradeAuthority.into());
|
||||
}
|
||||
if !program_upgrade_authority_info.is_signer {
|
||||
return Err(GovernanceError::UpgradeAuthorityMustSign.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
//! Utility functions
|
||||
|
||||
pub mod account;
|
||||
|
||||
pub mod token;
|
||||
|
||||
pub mod asserts;
|
||||
|
||||
pub mod bpf_loader_upgradeable;
|
|
@ -1,211 +0,0 @@
|
|||
//! General purpose SPL token utility functions
|
||||
|
||||
use arrayref::array_ref;
|
||||
use solana_program::{
|
||||
account_info::AccountInfo,
|
||||
entrypoint::ProgramResult,
|
||||
msg,
|
||||
program::{invoke, invoke_signed},
|
||||
program_error::ProgramError,
|
||||
program_pack::Pack,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
system_instruction,
|
||||
};
|
||||
|
||||
use crate::error::GovernanceError;
|
||||
|
||||
/// Creates and initializes SPL token account with PDA using the provided PDA seeds
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_spl_token_account_signed<'a>(
|
||||
payer_info: &AccountInfo<'a>,
|
||||
token_account_info: &AccountInfo<'a>,
|
||||
token_account_address_seeds: &[&[u8]],
|
||||
token_mint_info: &AccountInfo<'a>,
|
||||
token_account_owner_info: &AccountInfo<'a>,
|
||||
program_id: &Pubkey,
|
||||
system_info: &AccountInfo<'a>,
|
||||
spl_token_info: &AccountInfo<'a>,
|
||||
rent_sysvar_info: &AccountInfo<'a>,
|
||||
rent: &Rent,
|
||||
) -> Result<(), ProgramError> {
|
||||
let create_account_instruction = system_instruction::create_account(
|
||||
payer_info.key,
|
||||
token_account_info.key,
|
||||
1.max(rent.minimum_balance(spl_token::state::Account::get_packed_len())),
|
||||
spl_token::state::Account::get_packed_len() as u64,
|
||||
&spl_token::id(),
|
||||
);
|
||||
|
||||
let (account_address, bump_seed) =
|
||||
Pubkey::find_program_address(token_account_address_seeds, program_id);
|
||||
|
||||
if account_address != *token_account_info.key {
|
||||
msg!(
|
||||
"Create SPL Token Account with PDA: {:?} was requested while PDA: {:?} was expected",
|
||||
token_account_info.key,
|
||||
account_address
|
||||
);
|
||||
return Err(ProgramError::InvalidSeeds);
|
||||
}
|
||||
|
||||
let mut signers_seeds = token_account_address_seeds.to_vec();
|
||||
let bump = &[bump_seed];
|
||||
signers_seeds.push(bump);
|
||||
|
||||
invoke_signed(
|
||||
&create_account_instruction,
|
||||
&[
|
||||
payer_info.clone(),
|
||||
token_account_info.clone(),
|
||||
system_info.clone(),
|
||||
],
|
||||
&[&signers_seeds[..]],
|
||||
)?;
|
||||
|
||||
let initialize_account_instruction = spl_token::instruction::initialize_account(
|
||||
&spl_token::id(),
|
||||
token_account_info.key,
|
||||
token_mint_info.key,
|
||||
token_account_owner_info.key,
|
||||
)?;
|
||||
|
||||
invoke(
|
||||
&initialize_account_instruction,
|
||||
&[
|
||||
payer_info.clone(),
|
||||
token_account_info.clone(),
|
||||
token_account_owner_info.clone(),
|
||||
token_mint_info.clone(),
|
||||
spl_token_info.clone(),
|
||||
rent_sysvar_info.clone(),
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfers SPL Tokens
|
||||
pub fn transfer_spl_tokens<'a>(
|
||||
source_info: &AccountInfo<'a>,
|
||||
destination_info: &AccountInfo<'a>,
|
||||
authority_info: &AccountInfo<'a>,
|
||||
amount: u64,
|
||||
spl_token_info: &AccountInfo<'a>,
|
||||
) -> ProgramResult {
|
||||
let transfer_instruction = spl_token::instruction::transfer(
|
||||
&spl_token::id(),
|
||||
source_info.key,
|
||||
destination_info.key,
|
||||
authority_info.key,
|
||||
&[],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
invoke(
|
||||
&transfer_instruction,
|
||||
&[
|
||||
spl_token_info.clone(),
|
||||
authority_info.clone(),
|
||||
source_info.clone(),
|
||||
destination_info.clone(),
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfers SPL Tokens from a token account owned by the provided PDA authority with seeds
|
||||
pub fn transfer_spl_tokens_signed<'a>(
|
||||
source_info: &AccountInfo<'a>,
|
||||
destination_info: &AccountInfo<'a>,
|
||||
authority_info: &AccountInfo<'a>,
|
||||
authority_seeds: &[&[u8]],
|
||||
program_id: &Pubkey,
|
||||
amount: u64,
|
||||
spl_token_info: &AccountInfo<'a>,
|
||||
) -> ProgramResult {
|
||||
let (authority_address, bump_seed) = Pubkey::find_program_address(authority_seeds, program_id);
|
||||
|
||||
if authority_address != *authority_info.key {
|
||||
msg!(
|
||||
"Transfer SPL Token with Authority PDA: {:?} was requested while PDA: {:?} was expected",
|
||||
authority_info.key,
|
||||
authority_address
|
||||
);
|
||||
return Err(ProgramError::InvalidSeeds);
|
||||
}
|
||||
|
||||
let transfer_instruction = spl_token::instruction::transfer(
|
||||
&spl_token::id(),
|
||||
source_info.key,
|
||||
destination_info.key,
|
||||
authority_info.key,
|
||||
&[],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut signers_seeds = authority_seeds.to_vec();
|
||||
let bump = &[bump_seed];
|
||||
signers_seeds.push(bump);
|
||||
|
||||
invoke_signed(
|
||||
&transfer_instruction,
|
||||
&[
|
||||
spl_token_info.clone(),
|
||||
authority_info.clone(),
|
||||
source_info.clone(),
|
||||
destination_info.clone(),
|
||||
],
|
||||
&[&signers_seeds[..]],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Computationally cheap method to get amount from a token account
|
||||
/// It reads amount without deserializing full account data
|
||||
pub fn get_amount_from_token_account(
|
||||
token_account_info: &AccountInfo,
|
||||
) -> Result<u64, ProgramError> {
|
||||
if token_account_info.owner != &spl_token::id() {
|
||||
return Err(GovernanceError::InvalidTokenAccountOwner.into());
|
||||
}
|
||||
|
||||
// TokeAccount layout: mint(32), owner(32), amount(8), ...
|
||||
let data = token_account_info.try_borrow_data()?;
|
||||
let amount = array_ref![data, 64, 8];
|
||||
Ok(u64::from_le_bytes(*amount))
|
||||
}
|
||||
|
||||
/// Computationally cheap method to get mint from a token account
|
||||
/// It reads mint without deserializing full account data
|
||||
pub fn get_mint_from_token_account(
|
||||
token_account_info: &AccountInfo,
|
||||
) -> Result<Pubkey, ProgramError> {
|
||||
if token_account_info.owner != &spl_token::id() {
|
||||
return Err(GovernanceError::InvalidTokenAccountOwner.into());
|
||||
}
|
||||
|
||||
// TokeAccount layout: mint(32), owner(32), amount(8), ...
|
||||
let data = token_account_info.try_borrow_data()?;
|
||||
let mint_data = array_ref![data, 0, 32];
|
||||
Ok(Pubkey::new_from_array(*mint_data))
|
||||
}
|
||||
|
||||
/// Computationally cheap method to get owner from a token account
|
||||
/// It reads owner without deserializing full account data
|
||||
pub fn get_owner_from_token_account(
|
||||
token_account_info: &AccountInfo,
|
||||
) -> Result<Pubkey, ProgramError> {
|
||||
if token_account_info.owner != &spl_token::id() {
|
||||
return Err(GovernanceError::InvalidTokenAccountOwner.into());
|
||||
}
|
||||
|
||||
// TokeAccount layout: mint(32), owner(32), amount(8)
|
||||
let data = token_account_info.try_borrow_data()?;
|
||||
let owner_data = array_ref![data, 32, 32];
|
||||
Ok(Pubkey::new_from_array(*owner_data))
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
!*.so
|
Binary file not shown.
Binary file not shown.
|
@ -1,132 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod program_test;
|
||||
|
||||
use solana_program_test::tokio;
|
||||
|
||||
use program_test::*;
|
||||
|
||||
use spl_governance::error::GovernanceError;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_signatory() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Act
|
||||
let signatory_record_cookie = governance_test
|
||||
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let signatory_record_account = governance_test
|
||||
.get_signatory_record_account(&signatory_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(signatory_record_cookie.account, signatory_record_account);
|
||||
|
||||
let proposal_account = governance_test
|
||||
.get_proposal_account(&proposal_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(1, proposal_account.signatories_count);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_signatory_with_owner_or_delegate_must_sign_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let other_token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
token_owner_record_cookie.token_owner = other_token_owner_record_cookie.token_owner;
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
err,
|
||||
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_signatory_with_invalid_proposal_owner_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let other_token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
token_owner_record_cookie.address = other_token_owner_record_cookie.address;
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, GovernanceError::InvalidProposalOwnerAccount.into());
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
mod program_test;
|
||||
|
||||
use solana_program_test::*;
|
||||
|
||||
use program_test::*;
|
||||
use spl_governance::{error::GovernanceError, state::governance::GovernanceConfig};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_account_governance() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
// Act
|
||||
let account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let account_governance_account = governance_test
|
||||
.get_governance_account(&account_governance_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
account_governance_cookie.account,
|
||||
account_governance_account
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_account_governance_with_invalid_realm_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let mut realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
realm_cookie.address = account_governance_cookie.address;
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
|
||||
assert_eq!(err, GovernanceError::InvalidAccountType.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_account_governance_with_invalid_config_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
// Arrange below 50% threshold
|
||||
let config = GovernanceConfig {
|
||||
realm: realm_cookie.address,
|
||||
governed_account: governed_account_cookie.address,
|
||||
vote_threshold_percentage: 49, // below 50% threshold
|
||||
min_tokens_to_create_proposal: 1,
|
||||
min_instruction_hold_up_time: 1,
|
||||
max_voting_time: 1,
|
||||
};
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_account_governance_config(&realm_cookie, &governed_account_cookie, config)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
|
||||
assert_eq!(err, GovernanceError::InvalidGovernanceConfig.into());
|
||||
|
||||
// Arrange above 100% threshold
|
||||
let config = GovernanceConfig {
|
||||
realm: realm_cookie.address,
|
||||
governed_account: governed_account_cookie.address,
|
||||
vote_threshold_percentage: 101, // Above 100% threshold
|
||||
min_tokens_to_create_proposal: 1,
|
||||
min_instruction_hold_up_time: 1,
|
||||
max_voting_time: 1,
|
||||
};
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_account_governance_config(&realm_cookie, &governed_account_cookie, config)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
|
||||
assert_eq!(err, GovernanceError::InvalidGovernanceConfig.into());
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
mod program_test;
|
||||
|
||||
use solana_program_test::*;
|
||||
|
||||
use program_test::{tools::ProgramInstructionError, *};
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use spl_governance::{
|
||||
error::GovernanceError, tools::bpf_loader_upgradeable::get_program_upgrade_authority,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_program_governance() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_program_cookie = governance_test.with_governed_program().await;
|
||||
|
||||
// Act
|
||||
let program_governance_cookie = governance_test
|
||||
.with_program_governance(&realm_cookie, &governed_program_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let program_governance_account = governance_test
|
||||
.get_governance_account(&program_governance_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
program_governance_cookie.account,
|
||||
program_governance_account
|
||||
);
|
||||
|
||||
let program_data = governance_test
|
||||
.get_upgradable_loader_account(&governed_program_cookie.data_address)
|
||||
.await;
|
||||
|
||||
let upgrade_authority = get_program_upgrade_authority(&program_data).unwrap();
|
||||
|
||||
assert_eq!(Some(program_governance_cookie.address), upgrade_authority);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_program_governance_without_transferring_upgrade_authority() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut governed_program_cookie = governance_test.with_governed_program().await;
|
||||
|
||||
governed_program_cookie.transfer_upgrade_authority = false;
|
||||
|
||||
// Act
|
||||
let program_governance_cookie = governance_test
|
||||
.with_program_governance(&realm_cookie, &governed_program_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let program_governance_account = governance_test
|
||||
.get_governance_account(&program_governance_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
program_governance_cookie.account,
|
||||
program_governance_account
|
||||
);
|
||||
|
||||
let program_data = governance_test
|
||||
.get_upgradable_loader_account(&governed_program_cookie.data_address)
|
||||
.await;
|
||||
|
||||
let upgrade_authority = get_program_upgrade_authority(&program_data).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(governed_program_cookie.upgrade_authority.pubkey()),
|
||||
upgrade_authority
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_program_governance_without_transferring_upgrade_authority_with_invalid_authority_error(
|
||||
) {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut governed_program_cookie = governance_test.with_governed_program().await;
|
||||
|
||||
governed_program_cookie.transfer_upgrade_authority = false;
|
||||
governed_program_cookie.upgrade_authority = Keypair::new();
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_program_governance(&realm_cookie, &governed_program_cookie)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, GovernanceError::InvalidUpgradeAuthority.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_program_governance_without_transferring_upgrade_authority_with_authority_not_signed_error(
|
||||
) {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut governed_program_cookie = governance_test.with_governed_program().await;
|
||||
|
||||
governed_program_cookie.transfer_upgrade_authority = false;
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_program_governance_instruction(
|
||||
&realm_cookie,
|
||||
&governed_program_cookie,
|
||||
|i| {
|
||||
i.accounts[3].is_signer = false; // governed_program_upgrade_authority
|
||||
},
|
||||
Some(&[]),
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, GovernanceError::UpgradeAuthorityMustSign.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_program_governance_with_incorrect_upgrade_authority_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut governed_program_cookie = governance_test.with_governed_program().await;
|
||||
|
||||
governed_program_cookie.upgrade_authority = Keypair::new();
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_program_governance(&realm_cookie, &governed_program_cookie)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, ProgramInstructionError::IncorrectAuthority.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_program_governance_with_invalid_realm_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let mut realm_cookie = governance_test.with_realm().await;
|
||||
let governed_program_cookie = governance_test.with_governed_program().await;
|
||||
|
||||
let program_governance_cookie = governance_test
|
||||
.with_program_governance(&realm_cookie, &governed_program_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
realm_cookie.address = program_governance_cookie.address;
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_program_governance(&realm_cookie, &governed_program_cookie)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, GovernanceError::InvalidAccountType.into());
|
||||
}
|
|
@ -1,255 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program::instruction::AccountMeta;
|
||||
use solana_program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use program_test::*;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use spl_governance::error::GovernanceError;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_community_proposal_created() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let proposal_account = governance_test
|
||||
.get_proposal_account(&proposal_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(proposal_cookie.account, proposal_account);
|
||||
|
||||
let account_governance_account = governance_test
|
||||
.get_governance_account(&account_governance_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(1, account_governance_account.proposals_count);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_proposals_created() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let community_token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let council_token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
let community_proposal_cookie = governance_test
|
||||
.with_proposal(
|
||||
&community_token_owner_record_cookie,
|
||||
&mut account_governance_cookie,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let council_proposal_cookie = governance_test
|
||||
.with_proposal(
|
||||
&council_token_owner_record_cookie,
|
||||
&mut account_governance_cookie,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let community_proposal_account = governance_test
|
||||
.get_proposal_account(&community_proposal_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
community_proposal_cookie.account,
|
||||
community_proposal_account
|
||||
);
|
||||
|
||||
let council_proposal_account = governance_test
|
||||
.get_proposal_account(&council_proposal_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(council_proposal_cookie.account, council_proposal_account);
|
||||
|
||||
let account_governance_account = governance_test
|
||||
.get_governance_account(&account_governance_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(2, account_governance_account.proposals_count);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_proposal_with_not_authorized_governance_authority_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
token_owner_record_cookie.governance_authority = Some(Keypair::new());
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
err,
|
||||
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_proposal_with_governance_delegate_signer() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
governance_test
|
||||
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
|
||||
.await;
|
||||
|
||||
token_owner_record_cookie.governance_authority =
|
||||
Some(token_owner_record_cookie.clone_governance_delegate());
|
||||
|
||||
// Act
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let proposal_account = governance_test
|
||||
.get_proposal_account(&proposal_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(proposal_cookie.account, proposal_account);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_proposal_with_not_enough_tokens_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_amount = account_governance_cookie
|
||||
.account
|
||||
.config
|
||||
.min_tokens_to_create_proposal as u64
|
||||
- 1;
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit_amount(&realm_cookie, token_amount)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, GovernanceError::NotEnoughTokensToCreateProposal.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_proposal_with_invalid_token_owner_record_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let council_token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.with_proposal_instruction(
|
||||
&token_owner_record_cookie,
|
||||
&mut account_governance_cookie,
|
||||
|i| {
|
||||
// Set token_owner_record_address for different (Council) mint
|
||||
i.accounts[2] =
|
||||
AccountMeta::new_readonly(council_token_owner_record_cookie.address, false);
|
||||
},
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
err,
|
||||
GovernanceError::InvalidTokenOwnerRecordGoverningMint.into()
|
||||
);
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use program_test::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_realm_created() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
// Act
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
// Assert
|
||||
let realm_account = governance_test
|
||||
.get_realm_account(&realm_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(realm_cookie.account, realm_account);
|
||||
}
|
|
@ -1,258 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program::instruction::AccountMeta;
|
||||
use solana_program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use program_test::*;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use spl_governance::{error::GovernanceError, instruction::deposit_governing_tokens};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_initial_community_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
// Act
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(token_owner_record_cookie.account, token_owner_record);
|
||||
|
||||
let source_account = governance_test
|
||||
.get_token_account(&token_owner_record_cookie.token_source)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
token_owner_record_cookie.token_source_amount
|
||||
- token_owner_record_cookie
|
||||
.account
|
||||
.governing_token_deposit_amount,
|
||||
source_account.amount
|
||||
);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&realm_cookie.community_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
token_owner_record.governing_token_deposit_amount,
|
||||
holding_account.amount
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_initial_council_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
|
||||
|
||||
// Act
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(token_owner_record_cookie.account, token_owner_record);
|
||||
|
||||
let source_account = governance_test
|
||||
.get_token_account(&token_owner_record_cookie.token_source)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
token_owner_record_cookie.token_source_amount
|
||||
- token_owner_record_cookie
|
||||
.account
|
||||
.governing_token_deposit_amount,
|
||||
source_account.amount
|
||||
);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&council_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
token_owner_record.governing_token_deposit_amount,
|
||||
holding_account.amount
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_subsequent_community_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let deposit_amount = 5;
|
||||
let total_deposit_amount = token_owner_record_cookie
|
||||
.account
|
||||
.governing_token_deposit_amount
|
||||
+ deposit_amount;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.with_community_token_deposit(&realm_cookie, &token_owner_record_cookie, deposit_amount)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
total_deposit_amount,
|
||||
token_owner_record.governing_token_deposit_amount
|
||||
);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&realm_cookie.community_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(total_deposit_amount, holding_account.amount);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_subsequent_council_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let deposit_amount = 5;
|
||||
let total_deposit_amount = token_owner_record_cookie
|
||||
.account
|
||||
.governing_token_deposit_amount
|
||||
+ deposit_amount;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.with_council_token_deposit(&realm_cookie, &token_owner_record_cookie, deposit_amount)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
total_deposit_amount,
|
||||
token_owner_record.governing_token_deposit_amount
|
||||
);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&council_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(total_deposit_amount, holding_account.amount);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_initial_community_tokens_with_owner_must_sign_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let token_owner = Keypair::new();
|
||||
let transfer_authority = Keypair::new();
|
||||
let token_source = Keypair::new();
|
||||
|
||||
governance_test
|
||||
.create_token_account_with_transfer_authority(
|
||||
&token_source,
|
||||
&realm_cookie.account.community_mint,
|
||||
&realm_cookie.community_mint_authority,
|
||||
10,
|
||||
&token_owner,
|
||||
&transfer_authority.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut instruction = deposit_governing_tokens(
|
||||
&realm_cookie.address,
|
||||
&token_source.pubkey(),
|
||||
&token_owner.pubkey(),
|
||||
&transfer_authority.pubkey(),
|
||||
&governance_test.payer.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
instruction.accounts[3] = AccountMeta::new_readonly(token_owner.pubkey(), false);
|
||||
|
||||
// // Act
|
||||
|
||||
let error = governance_test
|
||||
.process_transaction(&[instruction], Some(&[&transfer_authority]))
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(error, GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn test_deposit_initial_community_tokens_with_invalid_owner_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let token_owner = Keypair::new();
|
||||
let transfer_authority = Keypair::new();
|
||||
let token_source = Keypair::new();
|
||||
|
||||
let invalid_owner = Keypair::new();
|
||||
|
||||
governance_test
|
||||
.create_token_account_with_transfer_authority(
|
||||
&token_source,
|
||||
&realm_cookie.account.community_mint,
|
||||
&realm_cookie.community_mint_authority,
|
||||
10,
|
||||
&token_owner,
|
||||
&transfer_authority.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let instruction = deposit_governing_tokens(
|
||||
&realm_cookie.address,
|
||||
&token_source.pubkey(),
|
||||
&invalid_owner.pubkey(),
|
||||
&transfer_authority.pubkey(),
|
||||
&governance_test.payer.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
// // Act
|
||||
|
||||
let error = governance_test
|
||||
.process_transaction(&[instruction], Some(&[&transfer_authority, &invalid_owner]))
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(error, GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
|
@ -1,219 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod program_test;
|
||||
|
||||
use solana_program_test::tokio;
|
||||
|
||||
use program_test::*;
|
||||
|
||||
use spl_governance::{error::GovernanceError, state::enums::ProposalState};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_signatory() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let signatory_record_cookie = governance_test
|
||||
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.remove_signatory(
|
||||
&proposal_cookie,
|
||||
&token_owner_record_cookie,
|
||||
&signatory_record_cookie,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let proposal_account = governance_test
|
||||
.get_proposal_account(&proposal_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(0, proposal_account.signatories_count);
|
||||
assert_eq!(ProposalState::Draft, proposal_account.state);
|
||||
|
||||
let signatory_account = governance_test
|
||||
.banks_client
|
||||
.get_account(signatory_record_cookie.address)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(None, signatory_account);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_signatory_with_owner_or_delegate_must_sign_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let signatory_record_cookie = governance_test
|
||||
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let other_token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
token_owner_record_cookie.token_owner = other_token_owner_record_cookie.token_owner;
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.remove_signatory(
|
||||
&proposal_cookie,
|
||||
&token_owner_record_cookie,
|
||||
&signatory_record_cookie,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
err,
|
||||
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_signatory_with_invalid_proposal_owner_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let signatory_record_cookie = governance_test
|
||||
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let other_token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
token_owner_record_cookie.address = other_token_owner_record_cookie.address;
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.remove_signatory(
|
||||
&proposal_cookie,
|
||||
&token_owner_record_cookie,
|
||||
&signatory_record_cookie,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, GovernanceError::InvalidProposalOwnerAccount.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_signatory_when_all_remaining_signed() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let signatory_record_cookie1 = governance_test
|
||||
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let signatory_record_cookie2 = governance_test
|
||||
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
governance_test
|
||||
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.remove_signatory(
|
||||
&proposal_cookie,
|
||||
&token_owner_record_cookie,
|
||||
&signatory_record_cookie2,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let proposal_account = governance_test
|
||||
.get_proposal_account(&proposal_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(1, proposal_account.signatories_count);
|
||||
assert_eq!(1, proposal_account.signatories_signed_off_count);
|
||||
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program::instruction::AccountMeta;
|
||||
use solana_program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use program_test::*;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use spl_governance::{error::GovernanceError, instruction::set_governance_delegate};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_community_governance_delegate() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
Some(token_owner_record_cookie.governance_delegate.pubkey()),
|
||||
token_owner_record.governance_delegate
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_governance_delegate_to_none() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
governance_test
|
||||
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.set_governance_delegate(
|
||||
&realm_cookie,
|
||||
&token_owner_record_cookie,
|
||||
&token_owner_record_cookie.token_owner,
|
||||
&realm_cookie.account.community_mint,
|
||||
&None,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(None, token_owner_record.governance_delegate);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_council_governance_delegate() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.with_council_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
Some(token_owner_record_cookie.governance_delegate.pubkey()),
|
||||
token_owner_record.governance_delegate
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_community_governance_delegate_with_owner_must_sign_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let hacker_governance_delegate = Keypair::new();
|
||||
|
||||
let mut instruction = set_governance_delegate(
|
||||
&token_owner_record_cookie.token_owner.pubkey(),
|
||||
&realm_cookie.address,
|
||||
&realm_cookie.account.community_mint,
|
||||
&token_owner_record_cookie.token_owner.pubkey(),
|
||||
&Some(hacker_governance_delegate.pubkey()),
|
||||
);
|
||||
|
||||
instruction.accounts[0] =
|
||||
AccountMeta::new_readonly(token_owner_record_cookie.token_owner.pubkey(), false);
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.process_transaction(&[instruction], None)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
err,
|
||||
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_community_governance_delegate_signed_by_governance_delegate() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
governance_test
|
||||
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
|
||||
.await;
|
||||
|
||||
let new_governance_delegate = Keypair::new();
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.set_governance_delegate(
|
||||
&realm_cookie,
|
||||
&token_owner_record_cookie,
|
||||
&token_owner_record_cookie.governance_delegate,
|
||||
&realm_cookie.account.community_mint,
|
||||
&Some(new_governance_delegate.pubkey()),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
Some(new_governance_delegate.pubkey()),
|
||||
token_owner_record.governance_delegate
|
||||
);
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod program_test;
|
||||
|
||||
use solana_program_test::tokio;
|
||||
|
||||
use program_test::*;
|
||||
use spl_governance::state::enums::ProposalState;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sign_off_proposal() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let governed_account_cookie = governance_test.with_governed_account().await;
|
||||
|
||||
let mut account_governance_cookie = governance_test
|
||||
.with_account_governance(&realm_cookie, &governed_account_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let proposal_cookie = governance_test
|
||||
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let signatory_record_cookie = governance_test
|
||||
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let proposal_account = governance_test
|
||||
.get_proposal_account(&proposal_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(1, proposal_account.signatories_count);
|
||||
assert_eq!(1, proposal_account.signatories_signed_off_count);
|
||||
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||
assert_eq!(Some(1), proposal_account.signing_off_at);
|
||||
assert_eq!(Some(1), proposal_account.voting_at);
|
||||
|
||||
let signatory_record_account = governance_test
|
||||
.get_signatory_record_account(&signatory_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(true, signatory_record_account.signed_off);
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use solana_program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use program_test::*;
|
||||
use solana_sdk::signature::Signer;
|
||||
|
||||
use spl_governance::{
|
||||
error::GovernanceError, instruction::withdraw_governing_tokens,
|
||||
state::token_owner_record::get_token_owner_record_address,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_withdraw_community_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(0, token_owner_record.governing_token_deposit_amount);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&realm_cookie.community_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(0, holding_account.amount);
|
||||
|
||||
let source_account = governance_test
|
||||
.get_token_account(&token_owner_record_cookie.token_source)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
token_owner_record_cookie.token_source_amount,
|
||||
source_account.amount
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_withdraw_council_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let token_owner_record = governance_test
|
||||
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(0, token_owner_record.governing_token_deposit_amount);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&realm_cookie.council_token_holding_account.unwrap())
|
||||
.await;
|
||||
|
||||
assert_eq!(0, holding_account.amount);
|
||||
|
||||
let source_account = governance_test
|
||||
.get_token_account(&token_owner_record_cookie.token_source)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
token_owner_record_cookie.token_source_amount,
|
||||
source_account.amount
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_withdraw_community_tokens_with_owner_must_sign_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let hacker_token_destination = Pubkey::new_unique();
|
||||
|
||||
let mut instruction = withdraw_governing_tokens(
|
||||
&realm_cookie.address,
|
||||
&hacker_token_destination,
|
||||
&token_owner_record_cookie.token_owner.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
instruction.accounts[3] =
|
||||
AccountMeta::new_readonly(token_owner_record_cookie.token_owner.pubkey(), false);
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.process_transaction(&[instruction], None)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
|
||||
assert_eq!(err, GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_withdraw_community_tokens_with_token_owner_record_address_mismatch_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let token_owner_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let vote_record_address = get_token_owner_record_address(
|
||||
&realm_cookie.address,
|
||||
&realm_cookie.account.community_mint,
|
||||
&token_owner_record_cookie.token_owner.pubkey(),
|
||||
);
|
||||
|
||||
let hacker_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let mut instruction = withdraw_governing_tokens(
|
||||
&realm_cookie.address,
|
||||
&hacker_record_cookie.token_source,
|
||||
&hacker_record_cookie.token_owner.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
instruction.accounts[4] = AccountMeta::new(vote_record_address, false);
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.process_transaction(&[instruction], Some(&[&hacker_record_cookie.token_owner]))
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
GovernanceError::InvalidTokenOwnerRecordAccountAddress.into()
|
||||
);
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
use solana_program::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use spl_governance::state::{
|
||||
governance::Governance, realm::Realm, token_owner_record::TokenOwnerRecord,
|
||||
};
|
||||
use spl_governance::state::{proposal::Proposal, signatory_record::SignatoryRecord};
|
||||
|
||||
use super::tools::clone_keypair;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RealmCookie {
|
||||
pub address: Pubkey,
|
||||
|
||||
pub account: Realm,
|
||||
|
||||
pub community_mint_authority: Keypair,
|
||||
|
||||
pub community_token_holding_account: Pubkey,
|
||||
|
||||
pub council_mint_authority: Option<Keypair>,
|
||||
|
||||
pub council_token_holding_account: Option<Pubkey>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TokeOwnerRecordCookie {
|
||||
pub address: Pubkey,
|
||||
|
||||
pub account: TokenOwnerRecord,
|
||||
|
||||
pub token_source: Pubkey,
|
||||
|
||||
pub token_source_amount: u64,
|
||||
|
||||
pub token_owner: Keypair,
|
||||
|
||||
pub governance_authority: Option<Keypair>,
|
||||
|
||||
pub governance_delegate: Keypair,
|
||||
|
||||
pub governing_token_mint: Pubkey,
|
||||
}
|
||||
|
||||
impl TokeOwnerRecordCookie {
|
||||
pub fn get_governance_authority(&self) -> &Keypair {
|
||||
self.governance_authority
|
||||
.as_ref()
|
||||
.unwrap_or(&self.token_owner)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn clone_governance_delegate(&self) -> Keypair {
|
||||
clone_keypair(&self.governance_delegate)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GovernedProgramCookie {
|
||||
pub address: Pubkey,
|
||||
pub upgrade_authority: Keypair,
|
||||
pub data_address: Pubkey,
|
||||
pub transfer_upgrade_authority: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GovernedAccountCookie {
|
||||
pub address: Pubkey,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GovernanceCookie {
|
||||
pub address: Pubkey,
|
||||
pub account: Governance,
|
||||
pub next_proposal_index: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProposalCookie {
|
||||
pub address: Pubkey,
|
||||
pub account: Proposal,
|
||||
|
||||
pub proposal_owner: Pubkey,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignatoryRecordCookie {
|
||||
pub address: Pubkey,
|
||||
pub account: SignatoryRecord,
|
||||
pub signatory: Keypair,
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,45 +0,0 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use solana_program::{instruction::InstructionError, program_error::ProgramError};
|
||||
use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError};
|
||||
|
||||
/// TODO: Add to SDK
|
||||
/// Instruction errors not mapped in the sdk
|
||||
pub enum ProgramInstructionError {
|
||||
/// Incorrect authority provided
|
||||
IncorrectAuthority,
|
||||
}
|
||||
|
||||
impl From<ProgramInstructionError> for ProgramError {
|
||||
fn from(e: ProgramInstructionError) -> Self {
|
||||
ProgramError::Custom(e as u32)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_transaction_error(transport_error: TransportError) -> ProgramError {
|
||||
match transport_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => ProgramError::Custom(error_index),
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
instruction_error,
|
||||
)) => ProgramError::try_from(instruction_error).unwrap_or_else(|ie| match ie {
|
||||
InstructionError::IncorrectAuthority => {
|
||||
ProgramInstructionError::IncorrectAuthority.into()
|
||||
}
|
||||
_ => panic!("TEST-INSTRUCTION-ERROR {:?}", ie),
|
||||
}),
|
||||
|
||||
_ => panic!("TEST-TRANSPORT-ERROR: {:?}", transport_error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_keypair(source: &Keypair) -> Keypair {
|
||||
Keypair::from_bytes(&source.to_bytes()).unwrap()
|
||||
}
|
||||
|
||||
/// NOP (No Operation) Override function
|
||||
#[allow(non_snake_case)]
|
||||
pub fn NopOverride<T>(_: &mut T) {}
|
Binary file not shown.
Before Width: | Height: | Size: 203 KiB |
Binary file not shown.
Before Width: | Height: | Size: 225 KiB |
|
@ -12,18 +12,18 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
borsh = "0.8"
|
||||
borsh = "0.7.1"
|
||||
borsh-derive = "0.8.1"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
thiserror = "1.0"
|
||||
uint = "0.8"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "0.10"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -1,42 +1,35 @@
|
|||
//! Approximation calculations
|
||||
|
||||
use {
|
||||
num_traits::{CheckedShl, CheckedShr, PrimInt},
|
||||
std::cmp::Ordering,
|
||||
num_traits::{CheckedAdd, CheckedDiv, One, Zero},
|
||||
std::cmp::Eq,
|
||||
};
|
||||
|
||||
/// Calculate square root of the given number
|
||||
///
|
||||
/// Code lovingly adapted from the excellent work at:
|
||||
/// https://github.com/derekdreery/integer-sqrt-rs
|
||||
///
|
||||
/// The algorithm is based on the implementation in:
|
||||
/// https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_(base_2)
|
||||
pub fn sqrt<T: PrimInt + CheckedShl + CheckedShr>(radicand: T) -> Option<T> {
|
||||
match radicand.cmp(&T::zero()) {
|
||||
Ordering::Less => return None, // fail for less than 0
|
||||
Ordering::Equal => return Some(T::zero()), // do nothing for 0
|
||||
_ => {}
|
||||
const SQRT_ITERATIONS: u8 = 50;
|
||||
|
||||
/// Perform square root
|
||||
pub fn sqrt<T: CheckedAdd + CheckedDiv + One + Zero + Eq + Copy>(radicand: T) -> Option<T> {
|
||||
if radicand == T::zero() {
|
||||
return Some(T::zero());
|
||||
}
|
||||
|
||||
// Compute bit, the largest power of 4 <= n
|
||||
let max_shift: u32 = T::zero().leading_zeros() - 1;
|
||||
let shift: u32 = (max_shift - radicand.leading_zeros()) & !1;
|
||||
let mut bit = T::one().checked_shl(shift)?;
|
||||
|
||||
let mut n = radicand;
|
||||
let mut result = T::zero();
|
||||
while bit != T::zero() {
|
||||
let result_with_bit = result.checked_add(&bit)?;
|
||||
if n >= result_with_bit {
|
||||
n = n.checked_sub(&result_with_bit)?;
|
||||
result = result.checked_shr(1)?.checked_add(&bit)?;
|
||||
// A good initial guess is the average of the interval that contains the
|
||||
// input number. For all numbers, that will be between 1 and the given number.
|
||||
let one = T::one();
|
||||
let two = one.checked_add(&one)?;
|
||||
let mut guess = radicand.checked_div(&two)?.checked_add(&one)?;
|
||||
let mut last_guess = guess;
|
||||
for _ in 0..SQRT_ITERATIONS {
|
||||
// x_k+1 = (x_k + radicand / x_k) / 2
|
||||
guess = last_guess
|
||||
.checked_add(&radicand.checked_div(&last_guess)?)?
|
||||
.checked_div(&two)?;
|
||||
if last_guess == guess {
|
||||
break;
|
||||
} else {
|
||||
result = result.checked_shr(1)?;
|
||||
last_guess = guess;
|
||||
}
|
||||
bit = bit.checked_shr(2)?;
|
||||
}
|
||||
Some(result)
|
||||
Some(guess)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -43,16 +43,7 @@ pub enum MathInstruction {
|
|||
/// The multipier
|
||||
multiplier: u64,
|
||||
},
|
||||
/// Divide two u64 values
|
||||
///
|
||||
/// No accounts required for this instruction
|
||||
U64Divide {
|
||||
/// The dividend
|
||||
dividend: u64,
|
||||
/// The divisor
|
||||
divisor: u64,
|
||||
},
|
||||
/// Multiply two float values
|
||||
/// Multiply two float valies
|
||||
///
|
||||
/// No accounts required for this instruction
|
||||
F32Multiply {
|
||||
|
@ -61,7 +52,7 @@ pub enum MathInstruction {
|
|||
/// The multipier
|
||||
multiplier: f32,
|
||||
},
|
||||
/// Divide two float values
|
||||
/// Divide two float valies
|
||||
///
|
||||
/// No accounts required for this instruction
|
||||
F32Divide {
|
||||
|
@ -123,17 +114,6 @@ pub fn u64_multiply(multiplicand: u64, multiplier: u64) -> Instruction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create PreciseSquareRoot instruction
|
||||
pub fn u64_divide(dividend: u64, divisor: u64) -> Instruction {
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts: vec![],
|
||||
data: MathInstruction::U64Divide { dividend, divisor }
|
||||
.try_to_vec()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create PreciseSquareRoot instruction
|
||||
pub fn f32_multiply(multiplicand: f32, multiplier: f32) -> Instruction {
|
||||
Instruction {
|
||||
|
|
|
@ -3,36 +3,9 @@
|
|||
use {
|
||||
crate::{approximations::sqrt, instruction::MathInstruction, precise_number::PreciseNumber},
|
||||
borsh::BorshDeserialize,
|
||||
solana_program::{
|
||||
account_info::AccountInfo, entrypoint::ProgramResult, log::sol_log_compute_units, msg,
|
||||
pubkey::Pubkey,
|
||||
},
|
||||
solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey},
|
||||
};
|
||||
|
||||
/// u64_multiply
|
||||
#[inline(never)]
|
||||
fn u64_multiply(multiplicand: u64, multiplier: u64) -> u64 {
|
||||
multiplicand * multiplier
|
||||
}
|
||||
|
||||
/// u64_divide
|
||||
#[inline(never)]
|
||||
fn u64_divide(dividend: u64, divisor: u64) -> u64 {
|
||||
dividend / divisor
|
||||
}
|
||||
|
||||
/// f32_multiply
|
||||
#[inline(never)]
|
||||
fn f32_multiply(multiplicand: f32, multiplier: f32) -> f32 {
|
||||
multiplicand * multiplier
|
||||
}
|
||||
|
||||
/// f32_divide
|
||||
#[inline(never)]
|
||||
fn f32_divide(dividend: f32, divisor: f32) -> f32 {
|
||||
dividend / divisor
|
||||
}
|
||||
|
||||
/// Instruction processor
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
|
@ -44,25 +17,19 @@ pub fn process_instruction(
|
|||
MathInstruction::PreciseSquareRoot { radicand } => {
|
||||
msg!("Calculating square root using PreciseNumber");
|
||||
let radicand = PreciseNumber::new(radicand as u128).unwrap();
|
||||
sol_log_compute_units();
|
||||
let result = radicand.sqrt().unwrap().to_imprecise().unwrap() as u64;
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
MathInstruction::SquareRootU64 { radicand } => {
|
||||
msg!("Calculating u64 square root");
|
||||
sol_log_compute_units();
|
||||
let result = sqrt(radicand).unwrap();
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
MathInstruction::SquareRootU128 { radicand } => {
|
||||
msg!("Calculating u128 square root");
|
||||
sol_log_compute_units();
|
||||
let result = sqrt(radicand).unwrap();
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -71,17 +38,7 @@ pub fn process_instruction(
|
|||
multiplier,
|
||||
} => {
|
||||
msg!("Calculating U64 Multiply");
|
||||
sol_log_compute_units();
|
||||
let result = u64_multiply(multiplicand, multiplier);
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
MathInstruction::U64Divide { dividend, divisor } => {
|
||||
msg!("Calculating U64 Divide");
|
||||
sol_log_compute_units();
|
||||
let result = u64_divide(dividend, divisor);
|
||||
sol_log_compute_units();
|
||||
let result = multiplicand * multiplier;
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -90,17 +47,13 @@ pub fn process_instruction(
|
|||
multiplier,
|
||||
} => {
|
||||
msg!("Calculating f32 Multiply");
|
||||
sol_log_compute_units();
|
||||
let result = f32_multiply(multiplicand, multiplier);
|
||||
sol_log_compute_units();
|
||||
let result = multiplicand * multiplier;
|
||||
msg!("{}", result as u64);
|
||||
Ok(())
|
||||
}
|
||||
MathInstruction::F32Divide { dividend, divisor } => {
|
||||
msg!("Calculating f32 Divide");
|
||||
sol_log_compute_units();
|
||||
let result = f32_divide(dividend, divisor);
|
||||
sol_log_compute_units();
|
||||
let result = dividend / divisor;
|
||||
msg!("{}", result as u64);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ async fn test_sqrt_u128() {
|
|||
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
|
||||
|
||||
// Dial down the BPF compute budget to detect if the operation gets bloated in the future
|
||||
pc.set_bpf_compute_max_units(4_000);
|
||||
pc.set_bpf_compute_max_units(5_500);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = pc.start().await;
|
||||
|
||||
|
@ -78,7 +78,8 @@ async fn test_sqrt_u128() {
|
|||
async fn test_sqrt_u128_max() {
|
||||
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
|
||||
|
||||
pc.set_bpf_compute_max_units(6_000);
|
||||
// This is pretty big too!
|
||||
pc.set_bpf_compute_max_units(90_000);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = pc.start().await;
|
||||
|
||||
|
@ -102,20 +103,6 @@ async fn test_u64_multiply() {
|
|||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_u64_divide() {
|
||||
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
|
||||
|
||||
pc.set_bpf_compute_max_units(1650);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = pc.start().await;
|
||||
|
||||
let mut transaction =
|
||||
Transaction::new_with_payer(&[instruction::u64_divide(3, 1)], Some(&payer.pubkey()));
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_f32_multiply() {
|
||||
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "spl-memo"
|
||||
version = "3.0.1"
|
||||
version = "3.0.0"
|
||||
description = "Solana Program Library Memo"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
|
@ -12,11 +12,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
solana-program = "1.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -117,7 +117,6 @@ async fn test_memo_signing() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_memo_compute_limits() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
src/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
target
|
||||
.vscode
|
||||
hfuzz*
|
||||
third_party/
|
||||
|
||||
*/node_modules
|
||||
js/dist
|
||||
js/lib
|
||||
js/docs
|
||||
js/src/secret.ts
|
|
@ -1,11 +0,0 @@
|
|||
# Name Service program
|
||||
|
||||
A spl program for issuing and managing ownership of: domain names, Solana
|
||||
Pubkeys, URLs, twitter handles, ipfs cid's, metadata, etc..
|
||||
|
||||
This program provides an interface and implementation that third parties can
|
||||
utilize to create and use their own version of a name service of any kind.
|
||||
|
||||
Full documentation is available at https://spl.solana.com/name-service
|
||||
|
||||
JavaScript binding are available in the `./js` directory.
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": { "project": "./tsconfig.json" },
|
||||
"env": { "es6": true },
|
||||
"ignorePatterns": ["node_modules", "build", "coverage"],
|
||||
"plugins": ["import", "eslint-comments"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:eslint-comments/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
"prettier/@typescript-eslint"
|
||||
],
|
||||
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"eslint-comments/disable-enable-pair": [
|
||||
"error",
|
||||
{ "allowWholeFile": true }
|
||||
],
|
||||
"eslint-comments/no-unused-disable": "error",
|
||||
"import/order": [
|
||||
"error",
|
||||
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
|
||||
],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{ "ignoreDeclarationSort": true, "ignoreCase": true }
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
# Name Service JavaScript bindings
|
||||
|
||||
[](https://unpkg.com/@solana/spl-name-service@latest/) [](https://github.com/solana-labs/token-list/blob/b3fa86b3fdd9c817139e38641d46c5a892542a52/LICENSE)
|
||||
|
||||
Full documentation is available at https://spl.solana.com/name-service
|
||||
|
||||
JavaScript binding allow to interact with a spl program for issuing and managing
|
||||
ownership of: domain names, Solana Pubkeys, URLs, twitter handles, arweave ids,
|
||||
metadata, etc..
|
||||
|
||||
This package provides an interface that third parties can
|
||||
utilize to create and use their own version of a name service of any kind.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @solana/spl-name-service
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn add @solana/spl-name-service
|
||||
```
|
File diff suppressed because it is too large
Load Diff
|
@ -1,67 +0,0 @@
|
|||
{
|
||||
"name": "@solana/spl-name-service",
|
||||
"version": "0.1.2",
|
||||
"description": "SPL Name Service JavaScript API",
|
||||
"license": "MIT",
|
||||
"author": "Solana Maintainers <maintainers@solana.foundation>",
|
||||
"homepage": "https://solana.com/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/solana-labs/solana-program-library"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/solana-labs/solana-program-library/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "tsc && node --trace-warnings dist/test.js",
|
||||
"build": "tsc",
|
||||
"prepublish": "tsc",
|
||||
"lint": "yarn pretty && eslint .",
|
||||
"lint:fix": "yarn pretty:fix && eslint . --fix",
|
||||
"pretty": "prettier --check 'src/*.[jt]s'",
|
||||
"pretty:fix": "prettier --write 'src/*.[jt]s'",
|
||||
"doc": "yarn typedoc src/index.ts"
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/recommended": "^1.0.1",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/node": "^14.14.20",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.1",
|
||||
"@typescript-eslint/parser": "^4.0.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.8.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-functional": "^3.0.2",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier": "^2.2.1",
|
||||
"save-dev": "0.0.1-security",
|
||||
"ts-node": "^9.1.1",
|
||||
"tslib": "^2.2.0",
|
||||
"typedoc": "^0.20.35",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solana/spl-token": "0.1.4",
|
||||
"@solana/web3.js": "^1.11.0",
|
||||
"bip32": "^2.0.6",
|
||||
"bn.js": "^5.1.3",
|
||||
"bs58": "4.0.1",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"core-util-is": "^1.0.2",
|
||||
"crypto": "^1.0.1",
|
||||
"crypto-ts": "^1.0.2",
|
||||
"fs": "^0.0.1-security",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
}
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
import {
|
||||
Connection,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
|
||||
import {
|
||||
createInstruction,
|
||||
deleteInstruction,
|
||||
transferInstruction,
|
||||
updateInstruction,
|
||||
} from './instructions';
|
||||
import { NameRegistryState } from './state';
|
||||
import { Numberu64 } from './utils';
|
||||
import {
|
||||
getHashedName,
|
||||
getNameAccountKey,
|
||||
getNameOwner,
|
||||
Numberu32,
|
||||
} from './utils';
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
export const NAME_PROGRAM_ID = new PublicKey(
|
||||
'namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX'
|
||||
);
|
||||
export const HASH_PREFIX = 'SPL Name Service';
|
||||
export const VERIFICATION_AUTHORITY_OFFSET = 64;
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
/**
|
||||
* Creates a name account with the given rent budget, allocated space, owner and class.
|
||||
*
|
||||
* @param connection The solana connection object to the RPC node
|
||||
* @param name The name of the new account
|
||||
* @param space The space in bytes allocated to the account
|
||||
* @param payerKey The allocation cost payer
|
||||
* @param nameOwner The pubkey to be set as owner of the new name account
|
||||
* @param lamports The budget to be set for the name account. If not specified, it'll be the minimum for rent exemption
|
||||
* @param nameClass The class of this new name
|
||||
* @param parentName The parent name of the new name. If specified its owner needs to sign
|
||||
* @returns
|
||||
*/
|
||||
export async function createNameRegistry(
|
||||
connection: Connection,
|
||||
name: string,
|
||||
space: number,
|
||||
payerKey: PublicKey,
|
||||
nameOwner: PublicKey,
|
||||
lamports?: number,
|
||||
nameClass?: PublicKey,
|
||||
parentName?: PublicKey
|
||||
): Promise<TransactionInstruction> {
|
||||
const hashed_name = await getHashedName(name);
|
||||
const nameAccountKey = await getNameAccountKey(
|
||||
hashed_name,
|
||||
nameClass,
|
||||
parentName
|
||||
);
|
||||
|
||||
space += 96; // Accounting for the Registry State Header
|
||||
|
||||
const balance = lamports
|
||||
? lamports
|
||||
: await connection.getMinimumBalanceForRentExemption(space);
|
||||
|
||||
let nameParentOwner: PublicKey | undefined;
|
||||
if (parentName) {
|
||||
const parentAccount = await getNameOwner(connection, parentName);
|
||||
nameParentOwner = parentAccount.owner;
|
||||
}
|
||||
|
||||
const createNameInstr = createInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
SystemProgram.programId,
|
||||
nameAccountKey,
|
||||
nameOwner,
|
||||
payerKey,
|
||||
hashed_name,
|
||||
new Numberu64(balance),
|
||||
new Numberu32(space),
|
||||
nameClass,
|
||||
parentName,
|
||||
nameParentOwner
|
||||
);
|
||||
|
||||
return createNameInstr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite the data of the given name registry.
|
||||
*
|
||||
* @param connection The solana connection object to the RPC node
|
||||
* @param name The name of the name registry to update
|
||||
* @param offset The offset to which the data should be written into the registry
|
||||
* @param input_data The data to be written
|
||||
* @param nameClass The class of this name, if it exsists
|
||||
* @param nameParent The parent name of this name, if it exists
|
||||
*/
|
||||
export async function updateNameRegistryData(
|
||||
connection: Connection,
|
||||
name: string,
|
||||
offset: number,
|
||||
input_data: Buffer,
|
||||
nameClass?: PublicKey,
|
||||
nameParent?: PublicKey
|
||||
): Promise<TransactionInstruction> {
|
||||
const hashed_name = await getHashedName(name);
|
||||
const nameAccountKey = await getNameAccountKey(
|
||||
hashed_name,
|
||||
nameClass,
|
||||
nameParent
|
||||
);
|
||||
|
||||
let signer: PublicKey;
|
||||
if (nameClass) {
|
||||
signer = nameClass;
|
||||
} else {
|
||||
signer = (await NameRegistryState.retrieve(connection, nameAccountKey))
|
||||
.owner;
|
||||
}
|
||||
|
||||
const updateInstr = updateInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
nameAccountKey,
|
||||
new Numberu32(offset),
|
||||
input_data,
|
||||
signer
|
||||
);
|
||||
|
||||
return updateInstr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the owner of a given name account.
|
||||
*
|
||||
* @param connection The solana connection object to the RPC node
|
||||
* @param name The name of the name account
|
||||
* @param newOwner The new owner to be set
|
||||
* @param curentNameOwner the current name Owner
|
||||
* @param nameClass The class of this name, if it exsists
|
||||
* @param nameParent The parent name of this name, if it exists
|
||||
* @returns
|
||||
*/
|
||||
export async function transferNameOwnership(
|
||||
connection: Connection,
|
||||
name: string,
|
||||
newOwner: PublicKey,
|
||||
nameClass?: PublicKey,
|
||||
nameParent?: PublicKey
|
||||
): Promise<TransactionInstruction> {
|
||||
const hashed_name = await getHashedName(name);
|
||||
const nameAccountKey = await getNameAccountKey(
|
||||
hashed_name,
|
||||
nameClass,
|
||||
nameParent
|
||||
);
|
||||
|
||||
let curentNameOwner: PublicKey;
|
||||
if (nameClass) {
|
||||
curentNameOwner = nameClass;
|
||||
} else {
|
||||
curentNameOwner = (
|
||||
await NameRegistryState.retrieve(connection, nameAccountKey)
|
||||
).owner;
|
||||
}
|
||||
|
||||
const transferInstr = transferInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
nameAccountKey,
|
||||
newOwner,
|
||||
curentNameOwner,
|
||||
nameClass
|
||||
);
|
||||
|
||||
return transferInstr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the name account and transfer the rent to the target.
|
||||
*
|
||||
* @param connection The solana connection object to the RPC node
|
||||
* @param name The name of the name account
|
||||
* @param refundTargetKey The refund destination address
|
||||
* @param nameClass The class of this name, if it exsists
|
||||
* @param nameParent The parent name of this name, if it exists
|
||||
* @returns
|
||||
*/
|
||||
export async function deleteNameRegistry(
|
||||
connection: Connection,
|
||||
name: string,
|
||||
refundTargetKey: PublicKey,
|
||||
nameClass?: PublicKey,
|
||||
nameParent?: PublicKey
|
||||
): Promise<TransactionInstruction> {
|
||||
const hashed_name = await getHashedName(name);
|
||||
const nameAccountKey = await getNameAccountKey(
|
||||
hashed_name,
|
||||
nameClass,
|
||||
nameParent
|
||||
);
|
||||
|
||||
let nameOwner: PublicKey;
|
||||
if (nameClass) {
|
||||
nameOwner = nameClass;
|
||||
} else {
|
||||
nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey))
|
||||
.owner;
|
||||
}
|
||||
|
||||
const changeAuthoritiesInstr = deleteInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
nameAccountKey,
|
||||
refundTargetKey,
|
||||
nameOwner
|
||||
);
|
||||
|
||||
return changeAuthoritiesInstr;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export * from './bindings';
|
||||
export * from './instructions';
|
||||
export * from './state';
|
||||
export * from './utils';
|
||||
export * from './twitter';
|
|
@ -1,198 +0,0 @@
|
|||
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
|
||||
|
||||
import { Numberu32, Numberu64 } from './utils';
|
||||
|
||||
export function createInstruction(
|
||||
nameProgramId: PublicKey,
|
||||
systemProgramId: PublicKey,
|
||||
nameKey: PublicKey,
|
||||
nameOwnerKey: PublicKey,
|
||||
payerKey: PublicKey,
|
||||
hashed_name: Buffer,
|
||||
lamports: Numberu64,
|
||||
space: Numberu32,
|
||||
nameClassKey?: PublicKey,
|
||||
nameParent?: PublicKey,
|
||||
nameParentOwner?: PublicKey
|
||||
): TransactionInstruction {
|
||||
const buffers = [
|
||||
Buffer.from(Int8Array.from([0])),
|
||||
new Numberu32(hashed_name.length).toBuffer(),
|
||||
hashed_name,
|
||||
lamports.toBuffer(),
|
||||
space.toBuffer(),
|
||||
];
|
||||
|
||||
const data = Buffer.concat(buffers);
|
||||
|
||||
const keys = [
|
||||
{
|
||||
pubkey: systemProgramId,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: payerKey,
|
||||
isSigner: true,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: nameKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: nameOwnerKey,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
|
||||
if (nameClassKey) {
|
||||
keys.push({
|
||||
pubkey: nameClassKey,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
});
|
||||
} else {
|
||||
keys.push({
|
||||
pubkey: new PublicKey(Buffer.alloc(32)),
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
if (nameParent) {
|
||||
keys.push({
|
||||
pubkey: nameParent,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
});
|
||||
} else {
|
||||
keys.push({
|
||||
pubkey: new PublicKey(Buffer.alloc(32)),
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
if (nameParentOwner) {
|
||||
keys.push({
|
||||
pubkey: nameParentOwner,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: nameProgramId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateInstruction(
|
||||
nameProgramId: PublicKey,
|
||||
nameAccountKey: PublicKey,
|
||||
offset: Numberu32,
|
||||
input_data: Buffer,
|
||||
nameUpdateSigner: PublicKey
|
||||
): TransactionInstruction {
|
||||
const buffers = [
|
||||
Buffer.from(Int8Array.from([1])),
|
||||
offset.toBuffer(),
|
||||
new Numberu32(input_data.length).toBuffer(),
|
||||
input_data,
|
||||
];
|
||||
|
||||
const data = Buffer.concat(buffers);
|
||||
const keys = [
|
||||
{
|
||||
pubkey: nameAccountKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: nameUpdateSigner,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: nameProgramId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function transferInstruction(
|
||||
nameProgramId: PublicKey,
|
||||
nameAccountKey: PublicKey,
|
||||
newOwnerKey: PublicKey,
|
||||
currentNameOwnerKey: PublicKey,
|
||||
nameClassKey?: PublicKey
|
||||
): TransactionInstruction {
|
||||
const buffers = [Buffer.from(Int8Array.from([2])), newOwnerKey.toBuffer()];
|
||||
|
||||
const data = Buffer.concat(buffers);
|
||||
|
||||
const keys = [
|
||||
{
|
||||
pubkey: nameAccountKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: currentNameOwnerKey,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
|
||||
if (nameClassKey) {
|
||||
keys.push({
|
||||
pubkey: nameClassKey,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: nameProgramId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteInstruction(
|
||||
nameProgramId: PublicKey,
|
||||
nameAccountKey: PublicKey,
|
||||
refundTargetKey: PublicKey,
|
||||
nameOwnerKey: PublicKey
|
||||
): TransactionInstruction {
|
||||
const buffers = [Buffer.from(Int8Array.from([3]))];
|
||||
|
||||
const data = Buffer.concat(buffers);
|
||||
const keys = [
|
||||
{
|
||||
pubkey: nameAccountKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: nameOwnerKey,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: refundTargetKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
];
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: nameProgramId,
|
||||
data,
|
||||
});
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import { Connection, PublicKey } from '@solana/web3.js';
|
||||
|
||||
export class NameRegistryState {
|
||||
parentName: PublicKey;
|
||||
owner: PublicKey;
|
||||
class: PublicKey;
|
||||
data: Buffer;
|
||||
|
||||
constructor(obj: {
|
||||
parentName: Uint8Array;
|
||||
owner: Uint8Array;
|
||||
class: Uint8Array;
|
||||
data: Uint8Array;
|
||||
}) {
|
||||
this.parentName = new PublicKey(obj.parentName);
|
||||
this.owner = new PublicKey(obj.owner);
|
||||
this.class = new PublicKey(obj.class);
|
||||
this.data = Buffer.from(obj.data);
|
||||
}
|
||||
|
||||
static deserialize(buffer: Buffer): NameRegistryState {
|
||||
return new NameRegistryState({
|
||||
parentName: buffer.slice(0, 32),
|
||||
owner: buffer.slice(32, 64),
|
||||
class: buffer.slice(64, 96),
|
||||
data: buffer.slice(96, buffer.length),
|
||||
});
|
||||
}
|
||||
|
||||
static async retrieve(
|
||||
connection: Connection,
|
||||
nameAccountKey: PublicKey
|
||||
): Promise<NameRegistryState> {
|
||||
const nameAccount = await connection.getAccountInfo(
|
||||
nameAccountKey,
|
||||
'processed'
|
||||
);
|
||||
if (!nameAccount) {
|
||||
throw new Error('Invalid name account provided');
|
||||
}
|
||||
|
||||
const res: NameRegistryState = NameRegistryState.deserialize(
|
||||
nameAccount.data
|
||||
);
|
||||
return res;
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
import { readFile } from 'fs/promises';
|
||||
|
||||
import { AccountInfo, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import { serialize } from 'borsh';
|
||||
import { sign } from 'tweetnacl';
|
||||
|
||||
import {
|
||||
createNameRegistry,
|
||||
deleteNameRegistry,
|
||||
transferNameOwnership,
|
||||
updateNameRegistryData,
|
||||
} from './bindings';
|
||||
import { NameRegistryState } from './state';
|
||||
import {
|
||||
getHashedName,
|
||||
getNameAccountKey,
|
||||
Numberu32,
|
||||
Numberu64,
|
||||
signAndSendTransactionInstructions,
|
||||
} from './utils';
|
||||
|
||||
const ENDPOINT = 'https://devnet.solana.com/';
|
||||
// const ENDPOINT = 'https://solana-api.projectserum.com/';
|
||||
|
||||
export async function test() {
|
||||
const connection = new Connection(ENDPOINT);
|
||||
// let secretKey = JSON.parse(
|
||||
// (await readFile('/home/lcchy-work/.config/solana/id_devnet.json')).toString()
|
||||
// );
|
||||
// let adminAccount = new Keypair(secretKey);
|
||||
|
||||
const root_name = '.sol';
|
||||
|
||||
// let create_instruction = await createNameRegistry(
|
||||
// connection,
|
||||
// root_name,
|
||||
// 1000,
|
||||
// adminAccount.publicKey,
|
||||
// adminAccount.publicKey,
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// await signAndSendTransactionInstructions(
|
||||
// connection,
|
||||
// [adminAccount],
|
||||
// adminAccount,
|
||||
// [create_instruction]
|
||||
// )
|
||||
// );
|
||||
|
||||
// let input_data = Buffer.from("Du");
|
||||
// let updateInstruction = await updateNameRegistryData(
|
||||
// connection,
|
||||
// root_name,
|
||||
// 0,
|
||||
// input_data,
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// await signAndSendTransactionInstructions(
|
||||
// connection,
|
||||
// [adminAccount],
|
||||
// adminAccount,
|
||||
// [updateInstruction]
|
||||
// )
|
||||
// );
|
||||
|
||||
// let transferInstruction = await transferNameOwnership(
|
||||
// connection,
|
||||
// root_name,
|
||||
// adminAccount.publicKey,
|
||||
// adminAccount.publicKey,
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// await signAndSendTransactionInstructions(
|
||||
// connection,
|
||||
// [adminAccount],
|
||||
// adminAccount,
|
||||
// [transferInstruction]
|
||||
// )
|
||||
// );
|
||||
|
||||
// let deleteInstruction = await deleteNameRegistry(
|
||||
// connection,
|
||||
// root_name,
|
||||
// adminAccount.publicKey
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// await signAndSendTransactionInstructions(
|
||||
// connection,
|
||||
// [adminAccount],
|
||||
// adminAccount,
|
||||
// [deleteInstruction]
|
||||
// )
|
||||
// );
|
||||
|
||||
const hashed_root_name = await getHashedName(root_name);
|
||||
const nameAccountKey = await getNameAccountKey(hashed_root_name);
|
||||
console.log(await NameRegistryState.retrieve(connection, nameAccountKey));
|
||||
}
|
||||
|
||||
test();
|
|
@ -1,343 +0,0 @@
|
|||
import {
|
||||
Connection,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
|
||||
import { NAME_PROGRAM_ID, VERIFICATION_AUTHORITY_OFFSET } from './bindings';
|
||||
import {
|
||||
createInstruction,
|
||||
deleteInstruction,
|
||||
transferInstruction,
|
||||
updateInstruction,
|
||||
} from './instructions';
|
||||
import { NameRegistryState } from './state';
|
||||
import {
|
||||
getFilteredProgramAccounts,
|
||||
getHashedName,
|
||||
getNameAccountKey,
|
||||
Numberu32,
|
||||
Numberu64,
|
||||
} from './utils';
|
||||
|
||||
export const TWITTER_VERIFICATION_AUTHORITY = new PublicKey(
|
||||
'867BLob5b52i81SNaV9Awm5ejkZV6VGSv9SxLcwukDDJ'
|
||||
);
|
||||
|
||||
// The address of the name registry that will be a parent to all twitter handle registries,
|
||||
// it should be owned by the TWITTER_VERIFICATION_AUTHORITY and it's name is irrelevant
|
||||
export const TWITTER_ROOT_PARENT_REGISTRY_KEY = new PublicKey(
|
||||
'AFrGkxNmVLBn3mKhvfJJABvm8RJkTtRhHDoaF97pQZaA'
|
||||
);
|
||||
// Signed by the authority and the payer
|
||||
export async function createVerifiedTwitterRegistry(
|
||||
connection: Connection,
|
||||
twitterHandle: string,
|
||||
verifiedPubkey: PublicKey,
|
||||
space: number, // The space that the user will have to write data into the verified registry
|
||||
payerKey: PublicKey
|
||||
): Promise<TransactionInstruction[]> {
|
||||
const hashedTwitterHandle = await getHashedName(twitterHandle);
|
||||
const twitterHandleRegistryKey = await getNameAccountKey(
|
||||
hashedTwitterHandle,
|
||||
undefined,
|
||||
TWITTER_ROOT_PARENT_REGISTRY_KEY
|
||||
);
|
||||
|
||||
const hashedVerifiedPubkey = await getHashedName(
|
||||
verifiedPubkey.toString().concat(twitterHandle)
|
||||
);
|
||||
const reverseRegistryKey = await getNameAccountKey(
|
||||
hashedVerifiedPubkey,
|
||||
TWITTER_VERIFICATION_AUTHORITY,
|
||||
undefined
|
||||
);
|
||||
|
||||
space += 96; // Accounting for the Registry State Header
|
||||
|
||||
const instructions = [
|
||||
// Create user facing registry
|
||||
createInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
SystemProgram.programId,
|
||||
twitterHandleRegistryKey,
|
||||
verifiedPubkey,
|
||||
payerKey,
|
||||
hashedTwitterHandle,
|
||||
new Numberu64(await connection.getMinimumBalanceForRentExemption(space)),
|
||||
new Numberu32(space),
|
||||
undefined,
|
||||
TWITTER_ROOT_PARENT_REGISTRY_KEY,
|
||||
TWITTER_VERIFICATION_AUTHORITY // Twitter authority acts as owner of the parent for all user-facing registries
|
||||
),
|
||||
// Create reverse lookup registry
|
||||
createInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
SystemProgram.programId,
|
||||
reverseRegistryKey,
|
||||
verifiedPubkey,
|
||||
payerKey,
|
||||
hashedVerifiedPubkey,
|
||||
new Numberu64(
|
||||
await connection.getMinimumBalanceForRentExemption(96 + 18)
|
||||
),
|
||||
new Numberu32(96 + 18), // maximum length of a twitter handle
|
||||
TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as class for all reverse-lookup registries
|
||||
undefined,
|
||||
undefined
|
||||
),
|
||||
// Write the twitter handle into the reverse lookup registry
|
||||
updateInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
reverseRegistryKey,
|
||||
new Numberu32(0),
|
||||
Buffer.from(twitterHandle),
|
||||
TWITTER_VERIFICATION_AUTHORITY
|
||||
),
|
||||
];
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
// Overwrite the data that is written in the user facing registry
|
||||
// Signed by the verified pubkey
|
||||
export async function changeTwitterRegistryData(
|
||||
twitterHandle: string,
|
||||
verifiedPubkey: PublicKey,
|
||||
offset: number, // The offset at which to write the input data into the NameRegistryData
|
||||
input_data: Buffer
|
||||
): Promise<TransactionInstruction[]> {
|
||||
const hashedTwitterHandle = await getHashedName(twitterHandle);
|
||||
const twitterHandleRegistryKey = await getNameAccountKey(
|
||||
hashedTwitterHandle,
|
||||
undefined,
|
||||
TWITTER_ROOT_PARENT_REGISTRY_KEY
|
||||
);
|
||||
|
||||
const instructions = [
|
||||
updateInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
twitterHandleRegistryKey,
|
||||
new Numberu32(offset),
|
||||
input_data,
|
||||
verifiedPubkey
|
||||
),
|
||||
];
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
// Change the verified pubkey for a given twitter handle
|
||||
// Signed by the Authority, the verified pubkey and the payer
|
||||
export async function changeVerifiedPubkey(
|
||||
connection: Connection,
|
||||
twitterHandle: string,
|
||||
currentVerifiedPubkey: PublicKey,
|
||||
newVerifiedPubkey: PublicKey,
|
||||
payerKey: PublicKey
|
||||
): Promise<TransactionInstruction[]> {
|
||||
const hashedTwitterHandle = await getHashedName(twitterHandle);
|
||||
const twitterHandleRegistryKey = await getNameAccountKey(
|
||||
hashedTwitterHandle,
|
||||
undefined,
|
||||
TWITTER_ROOT_PARENT_REGISTRY_KEY
|
||||
);
|
||||
|
||||
const currentHashedVerifiedPubkey = await getHashedName(
|
||||
currentVerifiedPubkey.toString().concat(twitterHandle)
|
||||
);
|
||||
const currentReverseRegistryKey = await getNameAccountKey(
|
||||
currentHashedVerifiedPubkey,
|
||||
TWITTER_VERIFICATION_AUTHORITY,
|
||||
undefined
|
||||
);
|
||||
|
||||
const newHashedVerifiedPubkey = await getHashedName(
|
||||
newVerifiedPubkey.toString().concat(twitterHandle)
|
||||
);
|
||||
const newReverseRegistryKey = await getNameAccountKey(
|
||||
newHashedVerifiedPubkey,
|
||||
TWITTER_VERIFICATION_AUTHORITY,
|
||||
undefined
|
||||
);
|
||||
|
||||
const instructions = [
|
||||
// Transfer the user-facing registry ownership
|
||||
transferInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
twitterHandleRegistryKey,
|
||||
newVerifiedPubkey,
|
||||
currentVerifiedPubkey,
|
||||
undefined
|
||||
),
|
||||
// Delete the current reverse registry
|
||||
deleteInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
currentReverseRegistryKey,
|
||||
payerKey,
|
||||
currentVerifiedPubkey
|
||||
),
|
||||
// Create the new reverse lookup registry
|
||||
createInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
SystemProgram.programId,
|
||||
newReverseRegistryKey,
|
||||
TWITTER_VERIFICATION_AUTHORITY,
|
||||
payerKey,
|
||||
newHashedVerifiedPubkey,
|
||||
new Numberu64(await connection.getMinimumBalanceForRentExemption(18)),
|
||||
new Numberu32(18), // maximum length of a twitter handle
|
||||
TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as class for all reverse-lookup registries
|
||||
undefined,
|
||||
undefined
|
||||
),
|
||||
// Write the twitter handle into the new reverse lookup registry
|
||||
updateInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
newReverseRegistryKey,
|
||||
new Numberu32(0),
|
||||
Buffer.from(twitterHandle),
|
||||
TWITTER_VERIFICATION_AUTHORITY
|
||||
),
|
||||
];
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
// Delete the verified registry for a given twitter handle
|
||||
// Signed by the verified pubkey
|
||||
export async function deleteTwitterRegistry(
|
||||
twitterHandle: string,
|
||||
verifiedPubkey: PublicKey
|
||||
): Promise<TransactionInstruction[]> {
|
||||
const hashedTwitterHandle = await getHashedName(twitterHandle);
|
||||
const twitterHandleRegistryKey = await getNameAccountKey(
|
||||
hashedTwitterHandle,
|
||||
undefined,
|
||||
TWITTER_ROOT_PARENT_REGISTRY_KEY
|
||||
);
|
||||
|
||||
const hashedVerifiedPubkey = await getHashedName(
|
||||
verifiedPubkey.toString().concat(twitterHandle)
|
||||
);
|
||||
const reverseRegistryKey = await getNameAccountKey(
|
||||
hashedVerifiedPubkey,
|
||||
TWITTER_VERIFICATION_AUTHORITY,
|
||||
undefined
|
||||
);
|
||||
|
||||
const instructions = [
|
||||
// Delete the user facing registry
|
||||
deleteInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
twitterHandleRegistryKey,
|
||||
verifiedPubkey,
|
||||
verifiedPubkey
|
||||
),
|
||||
// Delete the reverse registry
|
||||
deleteInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
reverseRegistryKey,
|
||||
verifiedPubkey,
|
||||
verifiedPubkey
|
||||
),
|
||||
];
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
export async function getTwitterHandle(
|
||||
connection: Connection,
|
||||
verifiedPubkey: PublicKey
|
||||
): Promise<string> {
|
||||
const filters = [
|
||||
{
|
||||
memcmp: {
|
||||
offset: 32,
|
||||
bytes: verifiedPubkey.toBase58(),
|
||||
},
|
||||
},
|
||||
{
|
||||
memcmp: {
|
||||
offset: VERIFICATION_AUTHORITY_OFFSET,
|
||||
bytes: TWITTER_VERIFICATION_AUTHORITY.toBase58(),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const filteredAccounts = await getFilteredProgramAccounts(
|
||||
connection,
|
||||
NAME_PROGRAM_ID,
|
||||
filters
|
||||
);
|
||||
|
||||
for (const f of filteredAccounts) {
|
||||
if (f.accountInfo.data.length == 114) {
|
||||
return f.accountInfo.data.slice(96, 114).toString();
|
||||
}
|
||||
}
|
||||
throw 'Could not find the twitter handle';
|
||||
}
|
||||
|
||||
// Returns the key of the user-facing registry
|
||||
export async function getTwitterRegistryKey(
|
||||
twitter_handle: string
|
||||
): Promise<PublicKey> {
|
||||
const hashedTwitterHandle = await getHashedName(twitter_handle);
|
||||
return await getNameAccountKey(
|
||||
hashedTwitterHandle,
|
||||
undefined,
|
||||
TWITTER_ROOT_PARENT_REGISTRY_KEY
|
||||
);
|
||||
}
|
||||
|
||||
export async function getTwitterRegistry(
|
||||
connection: Connection,
|
||||
twitter_handle: string
|
||||
): Promise<NameRegistryState> {
|
||||
const hashedTwitterHandle = await getHashedName(twitter_handle);
|
||||
const twitterHandleRegistryKey = await getNameAccountKey(
|
||||
hashedTwitterHandle,
|
||||
undefined,
|
||||
TWITTER_ROOT_PARENT_REGISTRY_KEY
|
||||
);
|
||||
const registry = NameRegistryState.retrieve(
|
||||
connection,
|
||||
twitterHandleRegistryKey
|
||||
);
|
||||
return registry;
|
||||
}
|
||||
|
||||
export async function getTwitterRegistryData(
|
||||
connection: Connection,
|
||||
verifiedPubkey: PublicKey
|
||||
): Promise<Buffer> {
|
||||
// Does not give you the name, but is faster than getTwitterHandle + getTwitterRegistry to get the data
|
||||
const filters = [
|
||||
{
|
||||
memcmp: {
|
||||
offset: 0,
|
||||
bytes: TWITTER_ROOT_PARENT_REGISTRY_KEY.toBytes(),
|
||||
},
|
||||
},
|
||||
{
|
||||
memcmp: {
|
||||
offset: 32,
|
||||
bytes: verifiedPubkey.toBytes(),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const filteredAccounts = await getFilteredProgramAccounts(
|
||||
connection,
|
||||
NAME_PROGRAM_ID,
|
||||
filters
|
||||
);
|
||||
|
||||
if (filteredAccounts.length > 1) {
|
||||
throw 'Found more than one twitter handle';
|
||||
}
|
||||
|
||||
return filteredAccounts[0].accountInfo.data;
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
import assert from 'assert';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
import {
|
||||
AccountInfo,
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
|
||||
import { HASH_PREFIX, NAME_PROGRAM_ID } from './bindings';
|
||||
import { NameRegistryState } from './state';
|
||||
|
||||
export class Numberu32 extends BN {
|
||||
/**
|
||||
* Convert to Buffer representation
|
||||
*/
|
||||
toBuffer(): Buffer {
|
||||
const a = super.toArray().reverse();
|
||||
const b = Buffer.from(a);
|
||||
if (b.length === 4) {
|
||||
return b;
|
||||
}
|
||||
assert(b.length < 4, 'Numberu32 too large');
|
||||
|
||||
const zeroPad = Buffer.alloc(4);
|
||||
b.copy(zeroPad);
|
||||
return zeroPad;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Numberu64 from Buffer representation
|
||||
*/
|
||||
static fromBuffer(buffer): BN {
|
||||
assert(buffer.length === 4, `Invalid buffer length: ${buffer.length}`);
|
||||
return new BN(
|
||||
[...buffer]
|
||||
.reverse()
|
||||
.map((i) => `00${i.toString(16)}`.slice(-2))
|
||||
.join(''),
|
||||
16
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Numberu64 extends BN {
|
||||
/**
|
||||
* Convert to Buffer representation
|
||||
*/
|
||||
toBuffer(): Buffer {
|
||||
const a = super.toArray().reverse();
|
||||
const b = Buffer.from(a);
|
||||
if (b.length === 8) {
|
||||
return b;
|
||||
}
|
||||
assert(b.length < 8, 'Numberu64 too large');
|
||||
|
||||
const zeroPad = Buffer.alloc(8);
|
||||
b.copy(zeroPad);
|
||||
return zeroPad;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Numberu64 from Buffer representation
|
||||
*/
|
||||
static fromBuffer(buffer): BN {
|
||||
assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`);
|
||||
return new BN(
|
||||
[...buffer]
|
||||
.reverse()
|
||||
.map((i) => `00${i.toString(16)}`.slice(-2))
|
||||
.join(''),
|
||||
16
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const signAndSendTransactionInstructions = async (
|
||||
// sign and send transaction
|
||||
connection: Connection,
|
||||
signers: Array<Keypair>,
|
||||
feePayer: Keypair,
|
||||
txInstructions: Array<TransactionInstruction>
|
||||
): Promise<string> => {
|
||||
const tx = new Transaction();
|
||||
tx.feePayer = feePayer.publicKey;
|
||||
signers.push(feePayer);
|
||||
tx.add(...txInstructions);
|
||||
return await connection.sendTransaction(tx, signers, {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
};
|
||||
|
||||
export async function getHashedName(name: string): Promise<Buffer> {
|
||||
const input = HASH_PREFIX + name;
|
||||
const buffer = createHash('sha256').update(input, 'utf8').digest();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export async function getNameAccountKey(
|
||||
hashed_name: Buffer,
|
||||
nameClass?: PublicKey,
|
||||
nameParent?: PublicKey
|
||||
): Promise<PublicKey> {
|
||||
const seeds = [hashed_name];
|
||||
if (nameClass) {
|
||||
seeds.push(nameClass.toBuffer());
|
||||
} else {
|
||||
seeds.push(Buffer.alloc(32));
|
||||
}
|
||||
if (nameParent) {
|
||||
seeds.push(nameParent.toBuffer());
|
||||
} else {
|
||||
seeds.push(Buffer.alloc(32));
|
||||
}
|
||||
const [nameAccountKey] = await PublicKey.findProgramAddress(
|
||||
seeds,
|
||||
NAME_PROGRAM_ID
|
||||
);
|
||||
return nameAccountKey;
|
||||
}
|
||||
|
||||
export async function getNameOwner(
|
||||
connection: Connection,
|
||||
nameAccountKey: PublicKey
|
||||
): Promise<NameRegistryState> {
|
||||
const nameAccount = await connection.getAccountInfo(nameAccountKey);
|
||||
if (!nameAccount) {
|
||||
throw 'Unable to find the given account.';
|
||||
}
|
||||
return NameRegistryState.retrieve(connection, nameAccountKey);
|
||||
}
|
||||
|
||||
//Taken from Serum
|
||||
export async function getFilteredProgramAccounts(
|
||||
connection: Connection,
|
||||
programId: PublicKey,
|
||||
filters
|
||||
): Promise<{ publicKey: PublicKey; accountInfo: AccountInfo<Buffer> }[]> {
|
||||
const resp = await connection.getProgramAccounts(programId, {
|
||||
commitment: connection.commitment,
|
||||
filters,
|
||||
encoding: 'base64',
|
||||
});
|
||||
return resp.map(
|
||||
({ pubkey, account: { data, executable, owner, lamports } }) => ({
|
||||
publicKey: pubkey,
|
||||
accountInfo: {
|
||||
data: data,
|
||||
executable,
|
||||
owner: owner,
|
||||
lamports,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
{
|
||||
"extends": "@tsconfig/recommended/tsconfig.json",
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"*" : ["types/*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2019",
|
||||
"outDir": "dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["node_modules/*", "src/types/*"]
|
||||
},
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/*"],
|
||||
"exclude": ["src/**/*.test.ts", "**/node_modules", "dist"]
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -1,29 +0,0 @@
|
|||
[package]
|
||||
name = "spl-name-service"
|
||||
description = "Solana Program Library Name Service"
|
||||
version = "0.1.0"
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
authors = ["lcchy <lucas@bonfida.com>"]
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
num-traits = "0.2"
|
||||
borsh = "0.8.1"
|
||||
num-derive = "0.3.3"
|
||||
thiserror = "1.0.24"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue