add governance classes and fetch proposals

This commit is contained in:
Maximilian Schneider 2021-08-17 01:20:17 +02:00
parent a18f4b38d4
commit 57bb583d64
62 changed files with 3848 additions and 2082 deletions

View File

@ -5,7 +5,6 @@ export interface EndpointInfo {
url: string
websocket: string
programId: string
poolKey: string
}
export interface TokenAccount {

View File

@ -1,121 +0,0 @@
//import LinkLeft from './LinkLeft'
import GradientText from './GradientText'
const ContentSectionAbout = () => {
return (
<div
id="about"
className="bg-bkg-2 transform -skew-y-3 pt-16 pb-16 mb-16 -mt-32 z-0"
>
<div className="max-w-7xl mx-auto px-8 pt-24 pb-16 transform skew-y-3">
<div className="py-16 ">
<div className="mb-16 max-w-4xl mx-auto text-center">
<h2 className="mb-4 text-3xl md:text-4xl lg:text-4xl text-white font-bold font-heading">
Why release <GradientText>MNGO</GradientText> token?
</h2>
<p className="text-xl md:text-2xl lg:text-2xl text-white text-opacity-70">
The MNGO token in its inception will serve 3 primary purposes.
</p>
</div>
<div className="overflow-hidden">
<div className="max-w-max lg:max-w-6xl mx-auto">
<div className="relative">
<div className="lg:grid lg:grid-cols-3 lg:gap-6">
<div className="lg:col-span-1">
<h2 className="text-2xl mb-4 leading-tight font-semibold font-heading">
Capitalize the Insurance Fund
</h2>
<p className="mb-8 text-base text-white text-opacity-70 leading-relaxed">
The Mango protocol relies on lenders to provide capital
for the others to use for trading and borrowing. The
capital in the Insurance Fund will be used to compensate
lenders in the unlikely event they incur losses.
</p>
</div>
<div className="lg:col-span-1">
<h2 className="text-2xl mb-4 leading-tight font-semibold font-heading">
Govern the Mango DAO
</h2>
<p className="mb-8 text-base text-white text-opacity-70 leading-relaxed">
MNGO tokens represent a direct stake in the Mango DAO. The
future direction of the Mango Protocol will be decided by
voting on proposals using MNGO tokens as the voting
mechanism.
</p>
</div>
<div className="lg:col-span-1">
<h2 className="text-2xl mb-4 leading-tight font-semibold font-heading">
Incentivize liquidity
</h2>
<p className="mb-8 text-base text-white text-opacity-70 leading-relaxed">
Bootstrapping liquidity is important in a new trading
system. Incentivizing market makers to provide it on our
order books with MNGO tokens will benefit the protocol and
its participants.
</p>
</div>
</div>
<div className="mt-10 py-5 px-5 bg-bkg-3 border border-bkg-4 shadow-md rounded-xl">
<h3 className="font-bold text-xl my-2">Token distribution</h3>
<div className="grid grid-cols-12 mt-4 py-1 px-1 rounded-md shadow-md bg-mango-med-dark">
<div className="col-span-10 bg-mango-green text-center rounded-l-sm py-1">
<span className="text-xs px-1 font-bold text-white">
90%
</span>
</div>
<div className="col-span-1 bg-mango-red text-center py-1">
<span className="text-xs px-1 font-bold text-white">
5%
</span>
</div>
<div className="col-span-1 bg-blue-400 text-center rounded-r-sm py-1">
<span className="text-xs px-1 font-bold text-white">
5%
</span>
</div>
</div>
<div className="grid grid-cols-3 mt-4">
<div className="col-span-3 md:col-span-1 lg:col-span-1 m-1 p-1">
<p className="text-mango-green font-bold text-base my-2">
Mango DAO
</p>
<p className="text-white text-opacity-70">
90% of MNGO tokens will be locked in a smart contract,
only accessible via DAO governance votes.
</p>
</div>
<div className="col-span-3 md:col-span-1 lg:col-span-1 m-1 p-1">
<p className="text-mango-red font-bold text-base my-2">
Insurance Fund{' '}
<span className="text-gray-400">(Token Sale)</span>
</p>
<p className="text-white text-opacity-70">
5% of MNGO tokens will be used to capitalize the
Insurance Fund that will protect lenders in the Mango
Protocol.
</p>
</div>
<div className="col-span-3 md:col-span-1 lg:col-span-1 m-1 p-1">
<p className="text-blue-400 font-bold text-base my-2">
Contributor tokens
</p>
<p className="text-white text-opacity-70">
5% will be allocated to a distributed group of early
contributors, who worked tirelessly on this project over
the last year. These tokens are unlocked.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default ContentSectionAbout

View File

@ -1,131 +0,0 @@
const ContentSectionLead = () => {
return (
<div className="bg-bkg-2 transform -skew-y-3 pt-12 pb-16 mb-16 -mt-32 z-0">
<div className="max-w-7xl mx-auto px-4 py-40 transform skew-y-3">
{/* Section 2 */}
<div className="max-w-4xl mb-12 mx-auto text-center">
<h2 className="mb-8 text-3xl md:text-4xl lg:text-4xl text-white font-bold font-heading">
How it works.
</h2>
<p className="mb-8 text-xl md:text-2xl lg:text-2xl text-white text-opacity-50">
We take the view that token sales should be simple, transparent and
minimize randomness and luck in the distribution.
</p>
</div>
<section className="">
<div className="grid grid-cols-3 gap-6 mb-6">
<div className="col-span-3 lg:col-span-1">
<div className="bg-bkg-3 border border-bkg-4 bg-feature-one bg-cover bg-bottom bg-no-repeat h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-mango-yellow font-semibold text-xl tracking-wide mb-2">
Deposit your USDC contribution.
</h2>
<p className="text-white text-opacity-50 text-base">
Users deposit USDC into a vault during the event period to
set their contribution amount.
</p>
</div>
</div>
</div>
</div>
<div className="col-span-3 lg:col-span-2">
<div className="bg-bkg-3 border border-bkg-4 bg-feature-two bg-contain lg:bg-cover bg-bottom bg-no-repeat h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-mango-yellow font-semibold text-xl tracking-wide mb-2">
48 hour participation period.
</h2>
<p className="text-white text-opacity-50 text-base">
The event will span over a 2 day period split into two
sections,{' '}
<span className="text-mango-green italic">
Unrestricted
</span>{' '}
and{' '}
<span className="text-mango-red italic">Restricted</span>.
</p>
<div className="flex flex-wrap overflow-hiddenm mt-8">
<div className="w-full overflow-hidden lg:w-1/2 pr-4 mt-4">
<p>
<span className="text-mango-green italic">
Unrestricted
</span>
</p>
<p className="text-white text-opacity-50">
During the unrestricted period users may deposit or
withdraw their USDC from the vault. During the
unrestricted period price can fluctuate.
</p>
</div>
<div className="w-full mt-4 overflow-hidden lg:w-1/2">
<p>
<span className="text-mango-red italic">
Restricted
</span>
</p>
<p className="text-white text-opacity-50">
After 24 hours deposits will be restricted and only
withdrawals allowed. During the restricted period
price can only go down.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-6">
<div className="col-span-3 lg:col-span-2">
<div className="bg-bkg-3 border border-bkg-4 bg-feature-three bg-contain lg:bg-cover bg-bottom bg-no-repeat h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-mango-yellow font-semibold text-xl tracking-wide mb-2">
Why does it work this way?
</h2>
<p className="text-white text-opacity-50 text-base mb-4">
Simple mechanisms are easier to build, explain, understand
and are harder to exploit. A transparent mechanism
increases participation because buyers are more confident
there are no hidden tricks that could harm them.
</p>
<p className="text-white text-opacity-50 text-base mb-4">
Elements of luck engineered into the mechanism distribute
value randomly to those who are most willing to do the
arbitrary, worthless tasks to get the free value.
</p>
{/*<p className="text-white font-bold leading-relaxed">
We believe all &quot;excess&quot; value should be captured
by token holders in the DAO.
</p>*/}
</div>
</div>
</div>
</div>
<div className="col-span-3 lg:col-span-1">
<div className="bg-bkg-3 bg-feature-four bg-cover bg-bottom bg-no-repeat h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-mango-yellow font-semibold text-xl tracking-wide mb-2">
MNGO unlocked and distributed.
</h2>
<p className="text-white text-opacity-50 text-base">
At event conclusion $MNGO gets distributed in propotion to
a users USDC contribution.{' '}
</p>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
)
}
export default ContentSectionLead

View File

@ -1,180 +0,0 @@
import Button from './Button'
import LinkLeft from './LinkLeft'
import GradientText from './GradientText'
const ContentSectionRedeem = () => {
return (
<>
{/* Section 2 */}
<div className="bg-bkg-2 transform -skew-y-3 pt-16 pb-0 mb-16 -mt-32 z-0 overflow-hidden">
<div className="px-8 pt-24 pb-16 z-0 transform skew-y-3">
<div className="max-w-7xl mx-auto py-16">
<div className="max-w-4xl mb-24 mx-auto text-center">
<h2 className="mb-4 text-3xl md:text-4xl lg:text-4xl text-white font-bold font-heading">
Want more <GradientText>MNGO</GradientText>?
</h2>
<p className="text-xl md:text-2xl lg:text-2xl text-white text-opacity-70">
These three steps support the protocol, we believe the DAO
should reward them.
</p>
</div>
<section className="">
<div className="grid grid-cols-3 gap-6 mb-24 pb-16">
<div className="col-span-3 lg:col-span-1">
<div className="bg-bkg-3 border border-bkg-4 bg-redeem-three bg-contain bg-bottom bg-no-repeat h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-white font-semibold text-xl tracking-wide mb-2">
Life is cool in the Raydium pool.
</h2>
<p className="mb-2 text-white text-opacity-70 text-base">
We want MNGO to be traded on Mango v3. So MNGO needs
decent liquidity on serum&apos;s order book.
<br />
<br />
It will be up to us MNGO holders to provide that
liquidity on day one. Let&apos;s start with a Raydium
pool until more sophisticated traders step in on their
own.
</p>
{/*
<a
rel="noreferrer"
target="_blank"
href="#"
>
<LinkLeft>Jump in Now</LinkLeft>
</a>
*/}
</div>
</div>
</div>
</div>
<div className="col-span-3 lg:col-span-1">
<div className="bg-bkg-3 border border-bkg-4 bg-redeem-two bg-cover bg-bottom bg-no-repeat h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-white font-semibold text-xl tracking-wide mb-2">
Become a Mango market maker.
</h2>
<p className="mb-2 text-white text-opacity-70 text-base">
Provide liquidity on the upcoming Perpetual Futures.
Start today on devnet with our example bot and get
ready for launch day.
<br />
<br />
Liquidity incentives for market making are built in
and instantly awarded.
</p>
<a
rel="noreferrer"
target="_blank"
href="https://docs.mango.markets/mango-v3/market-making-bot-python"
>
<LinkLeft>Learn more</LinkLeft>
</a>
</div>
</div>
</div>
</div>
<div className="col-span-3 lg:col-span-1">
<div className="bg-bkg-3 border border-bkg-4 bg-redeem-four bg-contain bg-bottom bg-no-repeat h-750 md:h-650 lg:h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-white font-semibold text-xl tracking-wide mb-2">
Build the best Mango.
</h2>
<p className="mb-2 text-white text-opacity-70 text-base">
This is by far the hardest and most rewarding method.
Launch a project that builds on top of Mango, help
grow the protocol.
<br />
<br />
The bar is high and quality is of the utmost
importance. We believe that the reward given out by
the DAO should be equally high.
</p>
{/* <a rel="noreferrer" target="_blank" href="#">
<LinkLeft>Learn More</LinkLeft>
</a> */}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<div className="transform skew-y-3">
<div className="max-w-4xl mx-auto text-center -mt-24">
<h2 className="mb-4 text-3xl md:text-4xl lg:text-4xl text-white font-bold font-heading">
With great power comes great responsibility.
</h2>
<p className="text-lg md:text-2xl lg:text-lg text-white text-opacity-70">
Mango is the first DAO on solana to use on-chain governance.
<br /> As token holders we all have a stake in driving the future
of this project.
</p>
<br />
{/* <p className="text-lg md:text-2xl lg:text-lg text-white text-opacity-70">
The governance mechanism is already functional and MNGO tokens are
used to both bring proposals to the DAO and vote on said
proposals. There&apos;ll be kinks to iron out as we get up and
running but as DAO members, we are all in this together.
</p> */}
<div className="py-12">
<a
rel="noreferrer"
target="_blank"
href="https://discord.gg/U5XSg5P9ut"
>
<Button>Get Involved</Button>
</a>
</div>
<div className="flex relative pt-12 -mt-12 lg:top-4 md:top-4 sm:top-4 xs:top-4">
<img className="h-96" alt="modals" src="../img/redeem1.png" />
</div>
</div>
</div>
</div>
{/*
<div className="mx-auto max-w-7xl py-16 my-16">
<div className="bg-bkg-3 border border-bkg-4 rounded-xl shadow-md overflow-hidden lg:grid lg:grid-cols-2 lg:gap-2 mt-8 bg-bg-texture bg-cover bg-bottom bg-no-repeat">
<div className="pt-10 pb-12 px-6 sm:pt-16 sm:px-16 lg:py-16 lg:pr-0 xl:py-20 xl:px-20 h-350">
<div className="lg:self-center">
<h2 className="text-3xl font-extrabold text-white sm:text-4xl">
<span className="block">The community that lives on Discord.</span>
</h2>
<p className="mt-4 text-xl leading-6 text-white text-opacity-50">
Join us in chat, we&apos;re always available and ready to answer any questions.
</p>
<div className="py-8">
<a
rel="noreferrer"
target="_blank"
href="https://discord.gg/67jySBhxrgs"
>
<Button>Get Involved</Button>
</a>
</div>
</div>
</div>
<div className="-mt-6 aspect-w-5 aspect-h-3 md:aspect-w-2 md:aspect-h-1">
<img
className="transform translate-x-2 translate-y-2 rounded-xl shadow-lg object-cover object-left-top sm:translate-x-12 lg:translate-y-16"
src="../img/redeem5.png"
alt="mango markets"
/>
</div>
</div>
</div>
*/}
</>
)
}
export default ContentSectionRedeem

View File

@ -1,99 +0,0 @@
import LinkLeft from './LinkLeft'
const ContentSectionRisks = () => {
return (
<>
{/* Section 3 */}
<div className="bg-bkg-2 transform -skew-y-3">
<div className="max-w-7xl mx-auto px-8 py-32 transform skew-y-3">
<div className="max-w-4xl mx-auto -mb-48 md:-mb-0 lg:-mb-0 text-center pb-16">
<h2 className="mb-4 text-3xl md:text-4xl lg:text-4xl text-white font-bold font-heading">
Transparency builds trust.
</h2>
<p className="text-xl md:text-2xl lg:text-2xl text-white text-opacity-70">
There are risks in participating in the token sale. It&apos;s
important you understand them before deciding to commit your
funds.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-7 lg:grid-cols-7 grid-rows-2 gap-6 mb-16 mx-auto">
<div className="col-span-1 md:col-span-3 lg:col-span-3 p-5 mt-48 md:mt-0 lg:mt-0 bg-bkg-3 border border-bkg-4 rounded-xl h-550 md:h-auto lg:h-auto w-auto z-10 shadow-md bg-risk-two md:bg-risk-one lg:bg-risk-one bg-contain bg-right-bottom bg-no-repeat">
<h3 className="text-white font-semibold text-xl tracking-wide my-2">
Unaudited smart contracts
</h3>
<div>
<p className="text-white text-opacity-70 text-base w-full md:w-1/2 lg:w-1/2">
We take great care and forethought in the way we design our
smart contracts. We make their source code publicly accessible
in order to get peer reviewed by as many experts possible.
<br />
<br />
Still we cannot guarantee that our products are free of
exploits, when we launch.
</p>
</div>
</div>
<div className="col-span-1 md:col-span-4 lg:col-span-4 row-span-2 p-5 bg-bkg-3 border border-bkg-4 rounded-xl h-auto w-auto z-10 shadow-md bg-risk-four md:bg-risk-three lg:bg-risk-three bg-contain bg-right-bottom bg-no-repeat">
<h3 className="text-white font-semibold text-xl tracking-wide my-2">
New token sale mechanism
</h3>
<p className="text-white text-opacity-70 text-base w-full md:w-3/4 lg:w-3/4">
The Mango token sale was designed with the goal of being as fair
as possible to all participants. However, there is a mechanism
by which one or more participants with large amounts of capital
could discourage others from participating in the token sale.
<br />
<br />
During the deposit phase, these participants could deposit very
large amounts of USDC. This would drive up the average price of
the token and potentially discourage others from participating
in the sale.
<br />
<br />
Then, during the last minute of the withdrawal phase, these
large participants could withdraw much of their USDC, thus
receiving a much lower average price, depending on how
successful they were at discouraging others.
<br />
<br />
Therefore, all participants should be aware of this potential
behaviour during the sale and make their best decisions
accordingly.
</p>
</div>
<div className="col-span-1 md:col-span-3 lg:col-span-3 p-5 bg-bkg-3 border border-bkg-4 rounded-xl h-auto w-auto z-10 shadow-md">
<h3 className="text-white font-semibold text-xl tracking-wide my-2">
Inflationary Tokenomics
</h3>
<p className="text-white text-opacity-70 text-base">
Mango will be running its own on-chain order books to allow
perpetual swap trading. In order to attract sophisticated
traders with the technical expertise to become market makers,
the protocol will need to provide very generous liquidity mining
rewards.
<br />
<br />
We were inspired by Bitcoin's emission schedule in our design,
but the mechanism is genuinely unproven in this context and
potentially could be exploited. Even if it operates correctly,
distributing MNGO from the DAO will be inflationary.
<br />
<br />
<a
rel="noreferrer"
target="_blank"
href="https://docs.mango.markets/mango-v3/liquidity-incentives"
>
<LinkLeft>Learn about it in the docs</LinkLeft>
</a>
</p>
</div>
</div>
</div>
</div>
</>
)
}
export default ContentSectionRisks

View File

@ -1,140 +0,0 @@
const ContentSectionSale = () => {
return (
<>
{/* Section 2 */}
<div className="pb-16 mb-16 z-0">
<div className="max-w-7xl mx-auto px-8 py-16">
<div className="max-w-4xl mb-24 mx-auto text-center">
<h2 className="mb-4 text-3xl md:text-4xl lg:text-4xl text-white font-bold font-heading">
How the token sale works.
</h2>
<p className="text-xl md:text-2xl lg:text-2xl text-white text-opacity-70">
Fairness and transparency for all participants.
</p>
</div>
<section className="">
<div className="grid grid-cols-3 gap-6 mb-6">
<div className="col-span-3 lg:col-span-2">
<div className="bg-bkg-3 border border-bkg-4 bg-feature-two bg-contain lg:bg-cover bg-bottom bg-no-repeat h-750 md:h-650 lg:h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-white font-semibold text-xl tracking-wide mb-2">
The token sale will span 48 hours
</h2>
<p className="text-white text-opacity-70 text-base">
The 48 hours consists of two 24 hour periods, the{' '}
<span className="text-mango-green text-base">
sale period
</span>{' '}
and the{' '}
<span className="text-blue-400 text-base">
grace period
</span>
. Only afterwards you will be able to redeem MNGO.
</p>
<div className="flex flex-wrap overflow-hiddenm mt-8">
<div className="w-full mb-4 lg:mb-0 overflow-hidden lg:w-1/2 pr-4">
<p className="mb-2">
<span className="font-semibold text-mango-green text-lg">
Sale period{' '}
</span>
</p>
<p className="text-base text-white text-opacity-70">
In the first 24 hours, you may deposit or withdraw
your USDC from the vault. During the sale period,
the MNGO price can fluctuate.
</p>
</div>
<div className="w-full overflow-hidden lg:w-1/2 pr-4">
<p className="mb-2">
<span className="font-semibold text-blue-400 text-lg">
Grace period{' '}
</span>
</p>
<p className="text-base text-white text-opacity-70">
After 24 hours, deposits will be restricted and only
withdrawals allowed. During the grace period, the
MNGO price can only go down.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-span-3 lg:col-span-1">
<div className="bg-bkg-3 border border-bkg-4 bg-feature-one bg-cover bg-bottom bg-no-repeat h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-white font-semibold text-xl tracking-wide mb-2">
Contribute your USDC
</h2>
<p className="text-white text-opacity-70 text-base">
During the
<span className="text-mango-green">
{' '}
sale period
</span>{' '}
you can deposit USDC into the vault. You can also change
this amount by withdrawing or depositing additional USDC
if you choose to.
</p>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-6">
<div className="col-span-3 lg:col-span-1">
<div className="bg-bkg-3 border border-bkg-4 bg-feature-four bg-cover bg-bottom bg-no-repeat h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-white font-semibold text-xl tracking-wide mb-2">
Redeem unlocked MNGO
</h2>
<p className="text-white text-opacity-70 text-base">
Once the{' '}
<span className="text-blue-400">grace period</span> ends
the MNGO tokens will be unlocked for redemption. The
number of tokens you&apos;ll receive will be
proportional to your USDC contribution.
</p>
</div>
</div>
</div>
</div>
<div className="col-span-3 lg:col-span-2">
<div className="bg-bkg-3 border border-bkg-4 bg-feature-three bg-contain lg:bg-cover bg-bottom bg-no-repeat h-650 w-full shadow-md rounded-xl overflow-hidden mx-auto">
<div className="py-4 px-8 mt-3">
<div className="flex flex-col mb-8">
<h2 className="text-white font-semibold text-xl tracking-wide mb-2">
Why does it work this way?
</h2>
<p className="text-white text-opacity-70 text-base mb-4">
We wanted to build a mechanism that is fair and
transparent for all participants. No private sale, no
backroom deals with VCs, all players are on a level
playing field. The mechanism is simple but robust. This
makes it easier to build, use, and more importantly,
harder to exploit.
</p>
<p className="text-white text-opacity-70 text-base">
All you need to do, is decide how much you contribute
and how much you value MNGO. If the sale price is too
high for you, you can still withdraw during the{' '}
<span className="text-blue-400">grace period</span>.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</>
)
}
export default ContentSectionSale

View File

@ -1,373 +0,0 @@
import { useEffect, useState } from 'react'
import {
ExclamationCircleIcon,
LockClosedIcon,
LockOpenIcon,
RefreshIcon,
} from '@heroicons/react/outline'
import useWalletStore from '../stores/useWalletStore'
import Input from './Input'
import Button from './Button'
import ConnectWalletButton from './ConnectWalletButton'
//import PoolCountdown from './PoolCountdown'
import Slider from './Slider'
import Loading from './Loading'
import WalletIcon from './WalletIcon'
import useLargestAccounts from '../hooks/useLargestAccounts'
//import useVaults from '../hooks/useVaults'
import usePool from '../hooks/usePool'
import styled from '@emotion/styled'
import 'twin.macro'
import { notify } from '../utils/notifications'
import useIpAddress from '../hooks/useIpAddress'
const SmallButton = styled.button``
const ContributionModal = () => {
const actions = useWalletStore((s) => s.actions)
const connected = useWalletStore((s) => s.connected)
const wallet = useWalletStore((s) => s.current)
const largestAccounts = useLargestAccounts()
//const vaults = useVaults()
const { endIdo, endDeposits } = usePool()
const { ipAllowed } = useIpAddress()
const usdcBalance = largestAccounts.usdc?.balance || 0
const redeemableBalance = largestAccounts.redeemable?.balance || 0
const totalBalance = usdcBalance + redeemableBalance
// const mangoRedeemable = vaults.usdc
// ? (redeemableBalance * vaults.mango.balance) / vaults.usdc.balance
// : 0
const [walletAmount, setWalletAmount] = useState(0)
const [contributionAmount, setContributionAmount] = useState(0)
const [submitting, setSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [editContribution, setEditContribution] = useState(false)
const [loading, setLoading] = useState(true)
const [maxButtonTransition, setMaxButtonTransition] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const [refreshing, setRefreshing] = useState(false)
const usdFormat = new Intl.NumberFormat('en-US')
//onst priceFormat = new Intl.NumberFormat('en-US', {
// maximumSignificantDigits: 4,
//})
useEffect(() => {
console.log('refresh modal on balance change')
setWalletAmount(usdcBalance)
setContributionAmount(redeemableBalance)
if (redeemableBalance > 0) {
setSubmitted(true)
}
}, [totalBalance])
const handleConnectDisconnect = () => {
if (connected) {
setSubmitted(false)
setEditContribution(false)
wallet.disconnect()
} else {
wallet.connect()
}
}
const handleSetContribution = () => {
setSubmitting(true)
setEditContribution(false)
}
const handleEditContribution = () => {
setEditContribution(true)
setSubmitted(false)
}
const onChangeAmountInput = (amount) => {
setWalletAmount(totalBalance - amount)
setContributionAmount(amount)
if (endDeposits?.isBefore() && amount > redeemableBalance) {
setErrorMessage('Deposits ended, contribution cannot increase')
setTimeout(() => setErrorMessage(null), 4000)
}
}
const onChangeSlider = (percentage) => {
let newContribution = Math.round(percentage * totalBalance) / 100
if (endDeposits?.isBefore() && newContribution > redeemableBalance) {
newContribution = redeemableBalance
setErrorMessage('Deposits ended, contribution cannot increase')
setTimeout(() => setErrorMessage(null), 4000)
}
setWalletAmount(totalBalance - newContribution)
setContributionAmount(newContribution)
}
const handleMax = () => {
if (endDeposits?.isAfter()) {
setWalletAmount(0)
setContributionAmount(totalBalance)
} else {
setWalletAmount(usdcBalance)
setContributionAmount(redeemableBalance)
}
setMaxButtonTransition(true)
}
const handleRefresh = async () => {
setRefreshing(true)
try {
await actions.fetchWalletTokenAccounts()
} finally {
setTimeout(() => setRefreshing(false), 1000)
}
}
useEffect(() => {
if (maxButtonTransition) {
setMaxButtonTransition(false)
}
}, [maxButtonTransition])
useEffect(() => {
setLoading(true)
if (largestAccounts.usdc) {
setLoading(false)
}
}, [largestAccounts])
useEffect(() => {
if (submitting) {
const handleSubmit = async () => {
try {
await actions.submitContribution(contributionAmount)
setSubmitted(true)
setSubmitting(false)
} catch (e) {
notify({ type: 'error', message: e.message })
console.error(e.message)
setSubmitted(false)
setSubmitting(false)
}
}
handleSubmit()
}
}, [submitting])
const hasUSDC = usdcBalance > 0 || redeemableBalance > 0
const difference = contributionAmount - redeemableBalance
const toLateToDeposit =
endDeposits?.isBefore() &&
endIdo.isAfter() &&
!largestAccounts.redeemable?.balance
const disableFormInputs =
submitted || !connected || loading || (connected && toLateToDeposit)
const dontAddMore =
endDeposits?.isBefore() && contributionAmount > redeemableBalance
const disableSubmit = disableFormInputs || difference == 0 || dontAddMore
return (
<>
<div className="flex-1 flex-col bg-bkg-2 border border-bkg-3 p-7 rounded-xl shadow-md z-10">
<div className="pb-4 text-center">
{!submitted &&
!submitting &&
!editContribution &&
!(connected && toLateToDeposit) && (
<>
<h2>The journey starts here.</h2>
<p>When you&apos;re ready, deposit your USDC.</p>
</>
)}
{!submitted &&
!submitting &&
!editContribution &&
connected &&
toLateToDeposit && (
<>
<h2>We&apos;re sorry, you missed it.</h2>
<p>The sale period has ended.</p>
</>
)}
{!submitted && submitting && (
<>
<h2>Approve the transaction.</h2>
<p>Almost there...</p>
</>
)}
{submitted && !submitting && (
<>
<h2>
You&apos;ve contributed ${usdFormat.format(contributionAmount)}.
</h2>
<p>Unlock to edit your contribution amount.</p>
</>
)}
{editContribution && !submitting && (
<>
<h2>
You&apos;ve contributed ${usdFormat.format(redeemableBalance)}.
</h2>
<p>
{endDeposits?.isBefore() && endIdo?.isAfter()
? 'You can only reduce your contribution during the grace period. Reducing cannot be reversed.'
: 'Increase or reduce your contribution.'}
</p>
</>
)}
</div>
{submitting ? (
<div className="flex h-64 items-center justify-center">
<Loading className="h-6 w-6 mb-3 text-primary-light" />
</div>
) : (
<>
<div
className={`${
connected ? 'opacity-100' : 'opacity-30'
} pb-6 transiton-all duration-1000 w-full`}
>
<div className="flex justify-between pb-2">
<div className="flex items-center text-xs text-fgd-4">
<a
onClick={handleRefresh}
className={
refreshing ? 'animate-spin' : 'hover:cursor-pointer'
}
>
<RefreshIcon
className={`w-4 h-4`}
style={{ transform: 'scaleX(-1)' }}
/>
</a>
<div title="Wallet Icon">
<WalletIcon className="w-4 h-4 mx-1 text-fgd-3 fill-current" />
</div>
{connected ? (
loading ? (
<div className="bg-bkg-4 rounded w-10 h-4 animate-pulse" />
) : (
<span
className="font-display text-fgd-3 ml-1"
title="Wallet USDC"
>
{usdFormat.format(walletAmount)}
</span>
)
) : (
'----'
)}
<img
alt=""
title="Wallet USDC"
width="16"
height="16"
src="/icons/usdc.svg"
className="ml-1 opacity-75"
/>
</div>
<div className="flex">
{submitted ? (
<SmallButton
className="ring-1 ring-secondary-1-light ring-inset hover:ring-secondary-1-dark hover:bg-transparent hover:text-secondary-1-dark font-normal rounded text-secondary-1-light text-xs py-0.5 px-1.5 mr-2"
disabled={!connected}
onClick={() => handleEditContribution()}
>
Unlock
</SmallButton>
) : null}
<SmallButton
className={`${
disableFormInputs && 'opacity-30'
} bg-bkg-4 hover:bg-bkg-3 font-normal rounded text-fgd-3 text-xs py-0.5 px-1.5`}
disabled={disableFormInputs}
onClick={() => handleMax()}
>
Max
</SmallButton>
</div>
</div>
<div className="flex items-center pb-4 relative">
{submitted ? (
<LockClosedIcon className="absolute text-secondary-2-light h-4 w-4 mb-0.5 left-2 z-10" />
) : null}
{editContribution ? (
<LockOpenIcon className="absolute text-secondary-1-light h-4 w-4 mb-0.5 left-2 z-10" />
) : null}
<Input
className={(submitted || editContribution) && 'pl-7'}
disabled={disableFormInputs}
type="text"
onChange={(e) => onChangeAmountInput(e.target.value)}
value={loading ? '' : contributionAmount}
suffix="USDC"
/>
</div>
<div
className={`${
!submitted ? 'opacity-100' : 'opacity-30'
} transiton-all duration-1000`}
>
<div className="pb-12">
<Slider
disabled={disableFormInputs}
value={(100 * contributionAmount) / totalBalance}
onChange={(v) => onChangeSlider(v)}
step={1}
maxButtonTransition={maxButtonTransition}
/>
</div>
<div className="h-12 pb-4">
{errorMessage && (
<div className="flex items-center pt-1.5 text-secondary-2-light">
<ExclamationCircleIcon className="h-4 w-4 mr-1.5" />
{errorMessage}
</div>
)}
</div>
{ipAllowed || !connected ? (
<Button
onClick={() => handleSetContribution()}
className="w-full py-2.5"
disabled={disableSubmit}
>
<div className={`flex items-center justify-center`}>
{dontAddMore
? "Sorry you can't add anymore 🥲"
: !hasUSDC && connected
? 'Your USDC balance is 0'
: difference >= 0
? `Deposit $${usdFormat.format(difference)}`
: `Withdraw $${usdFormat.format(-difference)}`}
</div>
</Button>
) : (
<Button className="w-full py-2.5" disabled>
<div className={`flex items-center justify-center`}>
Country Not Allowed 🇺🇸😭
</div>
</Button>
)}
</div>
</div>
<div className="flex items-center justify-center">
<ConnectWalletButton onClick={handleConnectDisconnect} />
</div>
</>
)}
</div>
</>
)
}
export default ContributionModal

View File

@ -1,46 +0,0 @@
import { ChevronDownIcon } from '@heroicons/react/solid'
import GradientText from './GradientText'
import Button from './Button'
function scrollToId(id: string) {
const element = document.getElementById(id)
const y = element.getBoundingClientRect().top + window.scrollY
window.scroll({
top: y,
behavior: 'smooth',
})
}
const HeroSection = () => {
return (
<section className="">
<div className="max-w-6xl px-8 mx-auto">
<div className="relative pt-16 md:pt-32 pb-2">
<div className="mb-8 mx-auto text-left md:text-center lg:text-center">
<h2 className="mb-4 text-3xl md:text-5xl lg:text-5xl text-white font-bold font-heading">
Claim your stake in the <GradientText>Mango DAO</GradientText>.
</h2>
<p className="text-xl md:text-2xl lg:text-2xl text-white text-opacity-70">
Join us in building Mango, the protocol for permissionless
leverage trading &amp; lending.
</p>
</div>
<div className="mb-16 flex flex-col items-center">
<a className="mb-6" onClick={() => scrollToId('contribute')}>
<Button>Contribute Now</Button>
</a>
<a
className="cursor-pointer flex flex-col items-center text-fgd-1 hover:underline"
onClick={() => scrollToId('about')}
>
<div>Learn More</div>
<ChevronDownIcon className="h-5 w-5" />
</a>
</div>
</div>
</div>
</section>
)
}
export default HeroSection

View File

@ -1,30 +0,0 @@
import GradientText from './GradientText'
//import usePool from '../hooks/usePool'
//import moment from 'moment-timezone'
const HeroSectionLead = () => {
// const { startIdo } = usePool()
return (
<section className="flex">
<div className="px-8 pb-24 mb-16 mx-auto h-auto justify-items-center align-middle">
<div className="relative pt-16 md:pt-32 pb-2">
<div className="max-w-2xl mb-16 mx-auto text-left md:text-center lg:text-center">
<h2 className="mb-8 text-7xl text-white font-bold font-heading">
<GradientText>WEN</GradientText> TOKEN?
</h2>
<p className="mb-8 text-2xl">
{/*
{startIdo
?.tz(moment.tz.guess())
?.format('dddd, MMMM Do YYYY, h:mm:ss A z')}
*/}
</p>
</div>
</div>
</div>
</section>
)
}
export default HeroSectionLead

View File

@ -1,25 +0,0 @@
import GradientText from './GradientText'
import RedeemModal from './RedeemModal'
const HeroSectionRedeem = () => {
return (
<section className="max-w-5xl mx-auto px-4">
<div className="flex flex-col md:flex-row lg:flex-row m-10 mx-auto gap-6 items-center">
<div className="flex-1 px-4">
<h2 className="mb-4 text-3xl md:text-5xl lg:text-5xl text-white font-bold font-heading">
That&apos;s a wrap! Your <GradientText>MNGO</GradientText> is ready.
</h2>
<p className="mb-2 text-xl text-white text-opacity-70">
Thank you to everyone who participated in the sale, you are now
valued members of the Mango DAO. Let&apos;s shape the future of
Mango together.
</p>
</div>
<div className="flex-1 my-5 z-10">
<RedeemModal />
</div>
</div>
</section>
)
}
export default HeroSectionRedeem

View File

@ -1,14 +0,0 @@
const MangoSale = () => {
return (
<div
className="inline-flex items-center relative h-6 -top-4 px-2 py-1 bg-gradient-to-br from-mango-red to-yellow-500
rounded-full"
>
<p className="text-white text-xs uppercase font-bold tracking-widest subpixel-antialiased">
Sale
</p>
</div>
)
}
export default MangoSale

View File

@ -1,25 +0,0 @@
import ContributionModal from './ContributionModal'
import StatsModal from './StatsModal'
const ModalSection = () => {
return (
<>
<div id="contribute" className="pt-32 pb-48 px-4">
<div className="max-w-5xl mx-auto text-center mb-12">
<h2 className="mb-4 text-3xl md:text-4xl lg:text-4xl text-white font-bold font-heading">
Ready to contribute?
</h2>
<p className="text-xl md:text-2xl lg:text-2xl text-white text-opacity-50">
Join us and become a valued stakeholder in the Mango DAO.
</p>
</div>
<div className="max-w-3xl flex flex-wrap md:flex-row lg:flex-row mx-auto">
<ContributionModal />
<StatsModal />
</div>
</div>
</>
)
}
export default ModalSection

View File

@ -1,45 +0,0 @@
import usePool from '../hooks/usePool'
import Countdown from 'react-countdown'
import moment from 'moment'
import { ClockIcon } from '@heroicons/react/outline'
const PoolCountdown = (props: { className?: string; date: moment.Moment }) => {
const { endIdo, endDeposits } = usePool()
const renderCountdown = ({ days, hours, minutes, seconds, completed }) => {
hours += days * 24
const message =
endDeposits?.isBefore() && endIdo?.isAfter()
? 'Deposits are closed'
: 'The IDO has ended'
if (completed) {
return <p className="text-mango-red text-xl">{message}</p>
} else {
return (
<div
className={`${props.className} font-bold text-white flex items-center`}
>
<ClockIcon className="w-5 h-5 mr-2 mt-0.5 text-fgd-3" />
<span className="text-xl">
{/* <span className="bg-bkg-1 border border-bkg-4 mx-0.5 px-1.5 py-1 rounded"> */}
{hours < 10 ? `0${hours}` : hours}
{/* </span> */}:
{/* <span className="bg-bkg-1 border border-bkg-4 mx-0.5 px-1.5 py-1 rounded"> */}
{minutes < 10 ? `0${minutes}` : minutes}
{/* </span> */}:
{/* <span className="bg-bkg-1 border border-bkg-4 mx-0.5 px-1.5 py-1 rounded"> */}
{seconds < 10 ? `0${seconds}` : seconds}
{/* </span> */}
</span>
</div>
)
}
}
if (props.date) {
return <Countdown date={props.date.format()} renderer={renderCountdown} />
} else {
return null
}
}
export default PoolCountdown

View File

@ -1,78 +0,0 @@
import usePool from '../hooks/usePool'
import useVaults from '../hooks/useVaults'
import PoolCountdown from './PoolCountdown'
const Card = (props: any) => {
return (
<div
className="flex-1 m-2 p-5 border border-bkg-3 rounded-xl h-auto w-auto z-10 shadow-md"
style={{ backgroundColor: 'rgba(44, 41, 66, 1)' }}
>
<p className="pb-2 text-white text-opacity-50 text-md">{props.title}</p>
{props.children}
</div>
)
}
const PoolInfoCards = () => {
const { endIdo, endDeposits } = usePool()
const vaults = useVaults()
//const numberFormat = new Intl.NumberFormat('en-US', {
// maximumFractionDigits: 10,
//})
return (
<div className="max-w-7xl flex flex-wrap mx-auto px-6 mb-16 z-10">
<Card title="Sale Period Ends">
<PoolCountdown date={endDeposits} />
</Card>
<Card title="Grace Period Ends">
<PoolCountdown date={endIdo} />
</Card>
<Card title="USDC Contributed">
<div className="flex">
<img
alt="USDC"
width="25"
height="25"
src="/icons/usdc.svg"
className={`mr-4`}
/>{' '}
<div className="font-bold text-fgd-1 text-xl">
{vaults.usdcBalance}
</div>
</div>
</Card>
<Card title="MNGO For Sale">
<div className="flex">
<img className="h-7 mr-2 w-auto" src="/logo.svg" alt="MNGO" />
<div className="font-bold text-fgd-1 text-xl">
{vaults.mangoBalance}
</div>
</div>
</Card>
{/*
<Card title="Estimated token price">
<div className="flex">
<img
alt="USDC"
width="25"
height="25"
src="/icons/usdc.svg"
className={`mr-2`}
/>{' '}
<div className="font-bold text-fgd-1 text-xl">
{vaults.estimatedPrice
? numberFormat.format(vaults.estimatedPrice)
: 'N/A'}
</div>
</div>
</Card>
*/}
</div>
)
}
export default PoolInfoCards

View File

@ -1,177 +0,0 @@
import { useEffect, useState } from 'react'
import useWalletStore from '../stores/useWalletStore'
import Button from './Button'
import Input from './Input'
import ConnectWalletButton from './ConnectWalletButton'
import Loading from './Loading'
import useLargestAccounts from '../hooks/useLargestAccounts'
import useVaults from '../hooks/useVaults'
import { calculateSupply } from '../utils/balance'
const RedeemModal = () => {
const actions = useWalletStore((s) => s.actions)
const wallet = useWalletStore((s) => s.current)
const connected = useWalletStore((s) => s.connected)
const redeemableMint = useWalletStore((s) => s.pool?.redeemableMint)
const mints = useWalletStore((s) => s.mints)
const largestAccounts = useLargestAccounts()
const vaults = useVaults()
const numberFormat = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 2,
})
const totalRaised = vaults.usdc?.balance
const redeemableBalance = largestAccounts.redeemable?.balance || 0
const redeemableSupply =
redeemableMint && calculateSupply(mints, redeemableMint)
const mangoAvailable =
vaults.mango && redeemableSupply
? (redeemableBalance * vaults.mango.balance) / redeemableSupply
: 0
const [submitting, setSubmitting] = useState(false)
const [loading, setLoading] = useState(true)
const handleConnectDisconnect = () => {
if (connected) {
wallet.disconnect()
} else {
wallet.connect()
}
}
const handleRedeem = () => {
setSubmitting(true)
}
useEffect(() => {
if (redeemableMint) {
actions.fetchMints()
}
}, [])
useEffect(() => {
setLoading(true)
if (largestAccounts.redeemable) {
setLoading(false)
}
}, [largestAccounts])
useEffect(() => {
if (submitting) {
const handleSubmit = async () => {
await actions.redeem()
setSubmitting(false)
}
handleSubmit()
}
}, [submitting])
const disableFormInputs = !connected || loading
const disableSubmit = disableFormInputs || redeemableBalance < 0
return (
<>
<div className="flex flex-col bg-bkg-2 border border-bkg-3 p-7 rounded-xl shadow-lg">
<div className="pb-4 text-center">
{!submitting ? (
<>
<h2>Redeem your MNGO</h2>
</>
) : null}
{submitting ? (
<>
<h2>Approve the transaction.</h2>
<p>Almost there...</p>
</>
) : null}
</div>
{submitting ? (
<div className="flex h-64 items-center justify-center">
<Loading className="h-6 w-6 mb-3 text-primary-light" />
</div>
) : (
<>
<div>
<span className="text-fgd-4 text-xs">Total raised</span>
<Input
className="border-0"
disabled
type="text"
value={numberFormat.format(totalRaised)}
suffix={
<img
alt=""
width="16"
height="16"
src="/icons/usdc.svg"
className="inline"
/>
}
/>
</div>
<div
className={`${
connected ? 'opacity-100' : 'opacity-30'
} pb-6 transiton-all duration-1000 w-full `}
>
<div className="py-1">
<span className="text-fgd-4 text-xs">Your contribution</span>
<Input
className="border-0"
disabled
type="text"
value={numberFormat.format(redeemableBalance)}
suffix={
<img
alt=""
width="16"
height="16"
src="/icons/usdc.svg"
className="inline"
/>
}
/>
</div>
<div className="py-1">
<span className="text-fgd-4 text-xs">Redeemable amount</span>
<Input
className="border-0"
disabled
type="text"
value={numberFormat.format(mangoAvailable)}
suffix={
<img
alt=""
width="16"
height="16"
src="/logo.svg"
className="inline"
/>
}
/>
</div>
<div className="py-6">
<Button
onClick={() => handleRedeem()}
className="w-full py-2.5"
disabled={disableSubmit}
>
<div className={`flex items-center justify-center`}>
Go MNGO
</div>
</Button>
</div>
</div>
<div className="flex justify-center">
<ConnectWalletButton onClick={handleConnectDisconnect} />
</div>
</>
)}
</div>
</>
)
}
export default RedeemModal

View File

@ -1,90 +0,0 @@
import PoolCountdown from './PoolCountdown'
import useVaults from '../hooks/useVaults'
import usePool from '../hooks/usePool'
import 'twin.macro'
const StatsModal = () => {
const vaults = useVaults()
const { endIdo, endDeposits } = usePool()
// const mangoRedeemable = vaults.usdc
// ? (redeemableBalance * vaults.mango.balance) / vaults.usdc.balance
// : 0
const priceFormat = new Intl.NumberFormat('en-US', {
minimumSignificantDigits: 4,
maximumSignificantDigits: 4,
})
return (
<>
<div className="flex-1 m-3 sm:-ml-8 bg-secondary-4-dark border border-bkg-3 py-6 rounded-xl shadow-md divide-y-2 divide-white divide-opacity-5 z-0">
<div className="pb-4 text-center">
<p className="text-fgd-3">Sale Period Ends</p>
<PoolCountdown date={endDeposits} className="justify-center pt-1" />
</div>
<div className="py-4 text-center">
<p className="text-fgd-3">Grace Period Ends</p>
<PoolCountdown date={endIdo} className="justify-center pt-1" />
</div>
<div className="py-4 text-center">
<p className="text-fgd-3">USDC Contributed</p>
<div className="flex items-center justify-center pt-0.5">
<img
alt=""
width="20"
height="20"
src="/icons/usdc.svg"
className={`mr-2`}
/>
<div className="font-bold text-fgd-1 text-xl">
{vaults.usdcBalance}
</div>
</div>
</div>
<div className="py-4 text-center">
<p className="text-fgd-3">Estimated Token Price</p>
<div className="flex items-center justify-center pt-0.5">
<img
alt=""
width="20"
height="20"
src="/icons/usdc.svg"
className={`mr-2`}
/>
<div className="font-bold text-fgd-1 text-xl">
{priceFormat.format(vaults.estimatedPrice)}
</div>
</div>
</div>
<div className="pt-4 text-center">
<p className="text-fgd-3">MNGO For Sale</p>
<div className="flex items-center justify-center pt-0.5">
<img className="h-5 mr-2 w-auto" src="/logo.svg" alt="mango" />
<div className="font-bold text-fgd-1 text-xl">
{vaults.mangoBalance}
</div>
</div>
{/* <p>
Start: {startIdo?.fromNow()} ({startIdo?.format()})
</p>
<p>
End Deposits: {endDeposits?.fromNow()} ({endDeposits?.format()})
</p>
<p>
End Withdraws: {endIdo?.fromNow()} ({endIdo?.format()})
</p>
<p>Current USDC in Pool: {vaults.usdc?.balance || 'N/A'}</p>
<p>Locked MNGO in Pool: {vaults.mango?.balance || 'N/A'}</p> */}
</div>
</div>
</>
)
}
export default StatsModal

View File

@ -1,4 +1,4 @@
import useWalletStore from '../stores/useWalletStore'
// import useWalletStore from '../stores/useWalletStore'
import { calculateBalance } from '../utils/balance'
import { ProgramAccount, TokenAccount } from '../utils/tokens'
@ -21,6 +21,7 @@ export function findLargestBalanceAccountForMint(
return { account, balance }
}
/*
export default function useLargestAccounts() {
const { pool, tokenAccounts, mints, usdcVault } = useWalletStore(
(state) => state
@ -37,3 +38,4 @@ export default function useLargestAccounts() {
: undefined
return { usdc, redeemable }
}
*/

View File

@ -1,22 +0,0 @@
import moment from 'moment'
import useWalletStore from '../stores/useWalletStore'
export default function usePool() {
const pool = useWalletStore((s) => s.pool)
const startIdo = pool ? moment.unix(pool.startIdoTs.toNumber()) : undefined
const endIdo = pool ? moment.unix(pool.endIdoTs.toNumber()) : undefined
const endDeposits = pool
? moment.unix(pool.endDepositsTs.toNumber())
: undefined
/*
// override for announcement
const unixTs = 1628553600
const startIdo = moment.unix(unixTs)
const endDeposits = moment.unix(unixTs).add(1, 'days')
const endIdo = moment.unix(unixTs).add(2, 'days')
*/
return { pool, startIdo, endIdo, endDeposits }
}

View File

@ -1,40 +0,0 @@
import { useMemo } from 'react'
import useWalletStore from '../stores/useWalletStore'
import { calculateBalance } from '../utils/balance'
export default function useVaults() {
const mints = useWalletStore((s) => s.mints)
const usdcVault = useWalletStore((s) => s.usdcVault)
const mangoVault = useWalletStore((s) => s.mangoVault)
const usdc = useMemo(
() =>
usdcVault
? { account: usdcVault, balance: calculateBalance(mints, usdcVault) }
: undefined,
[usdcVault, mints]
)
const mango = useMemo(
() =>
mangoVault
? { account: mangoVault, balance: calculateBalance(mints, mangoVault) }
: undefined,
[mangoVault, mints]
)
const usdcBalance = useMemo(
() => (usdc ? `${Math.round(usdc.balance).toLocaleString()}` : 'N/A'),
[usdc]
)
const mangoBalance = useMemo(
() => `${mango?.balance.toLocaleString()}` || 'N/A',
[mango]
)
const estimatedPrice = useMemo(
() => (usdc && mango ? usdc.balance / mango.balance : undefined),
[usdc, mango]
)
return { usdc, mango, usdcBalance, mangoBalance, estimatedPrice }
}

View File

@ -10,7 +10,6 @@ import {
import useInterval from './useInterval'
import useLocalStorageState from './useLocalStorageState'
import usePool from './usePool'
const SECONDS = 1000
@ -24,7 +23,6 @@ export default function useWallet() {
actions,
} = useWalletStore((state) => state)
const { endIdo } = usePool()
const [savedProviderUrl, setSavedProviderUrl] = useLocalStorageState(
'walletProvider',
DEFAULT_PROVIDER.url
@ -42,6 +40,7 @@ export default function useWallet() {
useEffect(() => {
if (provider) {
const updateWallet = () => {
console.log('updateWallet', setWalletStore)
// hack to also update wallet synchronously in case it disconnects
const wallet = new provider.adapter(
provider.url,
@ -80,7 +79,6 @@ export default function useWallet() {
'...' +
wallet.publicKey.toString().substr(-5),
})
await actions.fetchPool()
await actions.fetchWalletTokenAccounts()
})
wallet.on('disconnect', () => {
@ -101,23 +99,18 @@ export default function useWallet() {
}
}, [wallet])
// fetch pool on page load
// fetch on page load
useEffect(() => {
const pageLoad = async () => {
await actions.fetchPool()
actions.fetchMints()
console.log('pageLoad')
await actions.fetchProposals()
}
pageLoad()
}, [])
// refresh usdc vault regularly
// refresh regularly
useInterval(async () => {
if (endIdo.isAfter()) {
await actions.fetchUsdcVault()
} else {
await actions.fetchMNGOVault()
await actions.fetchRedeemableMint()
}
console.log('refresh')
}, 10 * SECONDS)
return { connected, wallet }

654
models/accounts.ts Normal file
View File

@ -0,0 +1,654 @@
import { PublicKey } from '@solana/web3.js'
import BN from 'bn.js'
/// Seed prefix for Governance Program PDAs
export const GOVERNANCE_PROGRAM_SEED = 'governance'
export enum GovernanceAccountType {
Uninitialized = 0,
Realm = 1,
TokenOwnerRecord = 2,
AccountGovernance = 3,
ProgramGovernance = 4,
Proposal = 5,
SignatoryRecord = 6,
VoteRecord = 7,
ProposalInstruction = 8,
MintGovernance = 9,
TokenGovernance = 10,
}
export interface GovernanceAccount {
accountType: GovernanceAccountType
}
export type GovernanceAccountClass =
| typeof Realm
| typeof TokenOwnerRecord
| typeof Governance
| typeof Proposal
| typeof SignatoryRecord
| typeof VoteRecord
| typeof ProposalInstruction
export function getAccountTypes(accountClass: GovernanceAccountClass) {
switch (accountClass) {
case Realm:
return [GovernanceAccountType.Realm]
case TokenOwnerRecord:
return [GovernanceAccountType.TokenOwnerRecord]
case Proposal:
return [GovernanceAccountType.Proposal]
case SignatoryRecord:
return [GovernanceAccountType.SignatoryRecord]
case VoteRecord:
return [GovernanceAccountType.VoteRecord]
case ProposalInstruction:
return [GovernanceAccountType.ProposalInstruction]
case Governance:
return [
GovernanceAccountType.AccountGovernance,
GovernanceAccountType.ProgramGovernance,
GovernanceAccountType.MintGovernance,
GovernanceAccountType.TokenGovernance,
]
default:
throw Error(`${accountClass} account is not supported`)
}
}
export enum VoteThresholdPercentageType {
YesVote = 0,
Quorum = 1,
}
export class VoteThresholdPercentage {
type = VoteThresholdPercentageType.YesVote
value: number
constructor(args: { value: number }) {
this.value = args.value
}
}
export enum VoteWeightSource {
Deposit,
Snapshot,
}
export enum InstructionExecutionStatus {
None,
Success,
Error,
}
export enum InstructionExecutionFlags {
None,
Ordered,
UseTransaction,
}
export enum MintMaxVoteWeightSourceType {
SupplyFraction = 0,
Absolute = 1,
}
export class MintMaxVoteWeightSource {
type = MintMaxVoteWeightSourceType.SupplyFraction
value: BN
constructor(args: { value: BN }) {
this.value = args.value
}
static SUPPLY_FRACTION_BASE = new BN(10000000000)
static SUPPLY_FRACTION_DECIMALS = 10
static FULL_SUPPLY_FRACTION = new MintMaxVoteWeightSource({
value: MintMaxVoteWeightSource.SUPPLY_FRACTION_BASE,
})
isFullSupply() {
return (
this.type === MintMaxVoteWeightSourceType.SupplyFraction &&
this.value.cmp(MintMaxVoteWeightSource.SUPPLY_FRACTION_BASE) === 0
)
}
getSupplyFraction() {
if (this.type !== MintMaxVoteWeightSourceType.SupplyFraction) {
throw new Error('Max vote weight is not fraction')
}
return this.value
}
}
export class RealmConfigArgs {
useCouncilMint: boolean
communityMintMaxVoteWeightSource: MintMaxVoteWeightSource
minCommunityTokensToCreateGovernance: BN
constructor(args: {
useCouncilMint: boolean
communityMintMaxVoteWeightSource: MintMaxVoteWeightSource
minCommunityTokensToCreateGovernance: BN
}) {
this.useCouncilMint = !!args.useCouncilMint
this.communityMintMaxVoteWeightSource =
args.communityMintMaxVoteWeightSource
this.minCommunityTokensToCreateGovernance =
args.minCommunityTokensToCreateGovernance
}
}
export class RealmConfig {
councilMint: PublicKey | undefined
communityMintMaxVoteWeightSource: MintMaxVoteWeightSource
minCommunityTokensToCreateGovernance: BN
reserved: Uint8Array
constructor(args: {
councilMint: PublicKey | undefined
communityMintMaxVoteWeightSource: MintMaxVoteWeightSource
minCommunityTokensToCreateGovernance: BN
reserved: Uint8Array
}) {
this.councilMint = args.councilMint
this.communityMintMaxVoteWeightSource =
args.communityMintMaxVoteWeightSource
this.minCommunityTokensToCreateGovernance =
args.minCommunityTokensToCreateGovernance
this.reserved = args.reserved
}
}
export class Realm {
accountType = GovernanceAccountType.Realm
communityMint: PublicKey
config: RealmConfig
reserved: Uint8Array
authority: PublicKey | undefined
name: string
constructor(args: {
communityMint: PublicKey
reserved: Uint8Array
config: RealmConfig
authority: PublicKey | undefined
name: string
}) {
this.communityMint = args.communityMint
this.config = args.config
this.reserved = args.reserved
this.authority = args.authority
this.name = args.name
}
}
export async function getTokenHoldingAddress(
programId: PublicKey,
realm: PublicKey,
governingTokenMint: PublicKey
) {
const [tokenHoldingAddress] = await PublicKey.findProgramAddress(
[
Buffer.from(GOVERNANCE_PROGRAM_SEED),
realm.toBuffer(),
governingTokenMint.toBuffer(),
],
programId
)
return tokenHoldingAddress
}
export class GovernanceConfig {
voteThresholdPercentage: VoteThresholdPercentage
minCommunityTokensToCreateProposal: BN
minInstructionHoldUpTime: number
maxVotingTime: number
voteWeightSource: VoteWeightSource
proposalCoolOffTime: number
minCouncilTokensToCreateProposal: BN
constructor(args: {
voteThresholdPercentage: VoteThresholdPercentage
minCommunityTokensToCreateProposal: BN
minInstructionHoldUpTime: number
maxVotingTime: number
voteWeightSource?: VoteWeightSource
proposalCoolOffTime?: number
minCouncilTokensToCreateProposal: BN
}) {
this.voteThresholdPercentage = args.voteThresholdPercentage
this.minCommunityTokensToCreateProposal =
args.minCommunityTokensToCreateProposal
this.minInstructionHoldUpTime = args.minInstructionHoldUpTime
this.maxVotingTime = args.maxVotingTime
this.voteWeightSource = args.voteWeightSource ?? VoteWeightSource.Deposit
this.proposalCoolOffTime = args.proposalCoolOffTime ?? 0
this.minCouncilTokensToCreateProposal =
args.minCouncilTokensToCreateProposal
}
}
export class Governance {
accountType: GovernanceAccountType
realm: PublicKey
governedAccount: PublicKey
config: GovernanceConfig
proposalCount: number
reserved?: Uint8Array
constructor(args: {
realm: PublicKey
governedAccount: PublicKey
accountType: number
config: GovernanceConfig
reserved?: Uint8Array
proposalCount: number
}) {
this.accountType = args.accountType
this.realm = args.realm
this.governedAccount = args.governedAccount
this.config = args.config
this.reserved = args.reserved
this.proposalCount = args.proposalCount
}
isProgramGovernance() {
return this.accountType === GovernanceAccountType.ProgramGovernance
}
isAccountGovernance() {
return this.accountType === GovernanceAccountType.AccountGovernance
}
isMintGovernance() {
return this.accountType === GovernanceAccountType.MintGovernance
}
isTokenGovernance() {
return this.accountType === GovernanceAccountType.TokenGovernance
}
}
export class TokenOwnerRecord {
accountType = GovernanceAccountType.TokenOwnerRecord
realm: PublicKey
governingTokenMint: PublicKey
governingTokenOwner: PublicKey
governingTokenDepositAmount: BN
unrelinquishedVotesCount: number
totalVotesCount: number
reserved: Uint8Array
governanceDelegate?: PublicKey
constructor(args: {
realm: PublicKey
governingTokenMint: PublicKey
governingTokenOwner: PublicKey
governingTokenDepositAmount: BN
unrelinquishedVotesCount: number
totalVotesCount: number
reserved: Uint8Array
}) {
this.realm = args.realm
this.governingTokenMint = args.governingTokenMint
this.governingTokenOwner = args.governingTokenOwner
this.governingTokenDepositAmount = args.governingTokenDepositAmount
this.unrelinquishedVotesCount = args.unrelinquishedVotesCount
this.totalVotesCount = args.totalVotesCount
this.reserved = args.reserved
}
}
export async function getTokenOwnerAddress(
programId: PublicKey,
realm: PublicKey,
governingTokenMint: PublicKey,
governingTokenOwner: PublicKey
) {
const [tokenOwnerRecordAddress] = await PublicKey.findProgramAddress(
[
Buffer.from(GOVERNANCE_PROGRAM_SEED),
realm.toBuffer(),
governingTokenMint.toBuffer(),
governingTokenOwner.toBuffer(),
],
programId
)
return tokenOwnerRecordAddress
}
export enum ProposalState {
Draft,
SigningOff,
Voting,
Succeeded,
Executing,
Completed,
Cancelled,
Defeated,
ExecutingWithErrors,
}
export class Proposal {
accountType = GovernanceAccountType.Proposal
governance: PublicKey
governingTokenMint: PublicKey
state: ProposalState
tokenOwnerRecord: PublicKey
signatoriesCount: number
signatoriesSignedOffCount: number
yesVotesCount: BN
noVotesCount: BN
instructionsExecutedCount: number
instructionsCount: number
instructionsNextIndex: number
draftAt: BN
signingOffAt: BN | null
votingAt: BN | null
votingAtSlot: BN | null
votingCompletedAt: BN | null
executingAt: BN | null
closedAt: BN | null
executionFlags: InstructionExecutionFlags
maxVoteWeight: BN | null
voteThresholdPercentage: VoteThresholdPercentage | null
name: string
descriptionLink: string
constructor(args: {
governance: PublicKey
governingTokenMint: PublicKey
state: ProposalState
tokenOwnerRecord: PublicKey
signatoriesCount: number
signatoriesSignedOffCount: number
descriptionLink: string
name: string
yesVotesCount: BN
noVotesCount: BN
draftAt: BN
signingOffAt: BN | null
votingAt: BN | null
votingAtSlot: BN | null
votingCompletedAt: BN | null
executingAt: BN | null
closedAt: BN | null
instructionsExecutedCount: number
instructionsCount: number
instructionsNextIndex: number
executionFlags: InstructionExecutionFlags
maxVoteWeight: BN | null
voteThresholdPercentage: VoteThresholdPercentage | null
}) {
this.governance = args.governance
this.governingTokenMint = args.governingTokenMint
this.state = args.state
this.tokenOwnerRecord = args.tokenOwnerRecord
this.signatoriesCount = args.signatoriesCount
this.signatoriesSignedOffCount = args.signatoriesSignedOffCount
this.descriptionLink = args.descriptionLink
this.name = args.name
this.yesVotesCount = args.yesVotesCount
this.noVotesCount = args.noVotesCount
this.draftAt = args.draftAt
this.signingOffAt = args.signingOffAt
this.votingAt = args.votingAt
this.votingAtSlot = args.votingAtSlot
this.votingCompletedAt = args.votingCompletedAt
this.executingAt = args.executingAt
this.closedAt = args.closedAt
this.instructionsExecutedCount = args.instructionsExecutedCount
this.instructionsCount = args.instructionsCount
this.instructionsNextIndex = args.instructionsNextIndex
this.executionFlags = args.executionFlags
this.maxVoteWeight = args.maxVoteWeight
this.voteThresholdPercentage = args.voteThresholdPercentage
}
/// Returns true if Proposal is in state when no voting can happen any longer
isVoteFinalized(): boolean {
switch (this.state) {
case ProposalState.Succeeded:
case ProposalState.Executing:
case ProposalState.Completed:
case ProposalState.Cancelled:
case ProposalState.Defeated:
case ProposalState.ExecutingWithErrors:
return true
case ProposalState.Draft:
case ProposalState.SigningOff:
case ProposalState.Voting:
return false
}
}
isFinalState(): boolean {
// 1) ExecutingWithErrors is not really a final state, it's undefined.
// However it usually indicates none recoverable execution error so we treat is as final for the ui purposes
// 2) Succeeded with no instructions is also treated as final since it can't transition any longer
// It really doesn't make any sense but until it's solved in the program we have to consider it as final in the ui
switch (this.state) {
case ProposalState.Completed:
case ProposalState.Cancelled:
case ProposalState.Defeated:
case ProposalState.ExecutingWithErrors:
return true
case ProposalState.Succeeded:
return this.instructionsCount === 0
case ProposalState.Executing:
case ProposalState.Draft:
case ProposalState.SigningOff:
case ProposalState.Voting:
return false
}
}
getStateTimestamp(): number {
switch (this.state) {
case ProposalState.Succeeded:
case ProposalState.Defeated:
return this.votingCompletedAt ? this.votingCompletedAt.toNumber() : 0
case ProposalState.Completed:
case ProposalState.Cancelled:
return this.closedAt ? this.closedAt.toNumber() : 0
case ProposalState.Executing:
case ProposalState.ExecutingWithErrors:
return this.executingAt ? this.executingAt.toNumber() : 0
case ProposalState.Draft:
return this.draftAt.toNumber()
case ProposalState.SigningOff:
return this.signingOffAt ? this.signingOffAt.toNumber() : 0
case ProposalState.Voting:
return this.votingAt ? this.votingAt.toNumber() : 0
}
}
getStateSortRank(): number {
// Always show proposals in voting state at the top
if (this.state === ProposalState.Voting) {
return 2
}
// Then show proposals in pending state and finalized at the end
return this.isFinalState() ? 0 : 1
}
/// Returns true if Proposal has not been voted on yet
isPreVotingState() {
return !this.votingAtSlot
}
}
export class SignatoryRecord {
accountType: GovernanceAccountType = GovernanceAccountType.SignatoryRecord
proposal: PublicKey
signatory: PublicKey
signedOff: boolean
constructor(args: {
proposal: PublicKey
signatory: PublicKey
signedOff: boolean
}) {
this.proposal = args.proposal
this.signatory = args.signatory
this.signedOff = !!args.signedOff
}
}
export async function getSignatoryRecordAddress(
programId: PublicKey,
proposal: PublicKey,
signatory: PublicKey
) {
const [signatoryRecordAddress] = await PublicKey.findProgramAddress(
[
Buffer.from(GOVERNANCE_PROGRAM_SEED),
proposal.toBuffer(),
signatory.toBuffer(),
],
programId
)
return signatoryRecordAddress
}
export class VoteWeight {
yes: BN
no: BN
constructor(args: { yes: BN; no: BN }) {
this.yes = args.yes
this.no = args.no
}
}
export class VoteRecord {
accountType = GovernanceAccountType.VoteRecord
proposal: PublicKey
governingTokenOwner: PublicKey
isRelinquished: boolean
voteWeight: VoteWeight
constructor(args: {
proposal: PublicKey
governingTokenOwner: PublicKey
isRelinquished: boolean
voteWeight: VoteWeight
}) {
this.proposal = args.proposal
this.governingTokenOwner = args.governingTokenOwner
this.isRelinquished = !!args.isRelinquished
this.voteWeight = args.voteWeight
}
}
export class AccountMetaData {
pubkey: PublicKey
isSigner: boolean
isWritable: boolean
constructor(args: {
pubkey: PublicKey
isSigner: boolean
isWritable: boolean
}) {
this.pubkey = args.pubkey
this.isSigner = !!args.isSigner
this.isWritable = !!args.isWritable
}
}
export class InstructionData {
programId: PublicKey
accounts: AccountMetaData[]
data: Uint8Array
constructor(args: {
programId: PublicKey
accounts: AccountMetaData[]
data: Uint8Array
}) {
this.programId = args.programId
this.accounts = args.accounts
this.data = args.data
}
}
export class ProposalInstruction {
accountType = GovernanceAccountType.ProposalInstruction
proposal: PublicKey
instructionIndex: number
holdUpTime: number
instruction: InstructionData
executedAt: BN | null
executionStatus: InstructionExecutionStatus
constructor(args: {
proposal: PublicKey
instructionIndex: number
holdUpTime: number
instruction: InstructionData
executedAt: BN | null
executionStatus: InstructionExecutionStatus
}) {
this.proposal = args.proposal
this.instructionIndex = args.instructionIndex
this.holdUpTime = args.holdUpTime
this.instruction = args.instruction
this.executedAt = args.executedAt
this.executionStatus = args.executionStatus
}
}

181
models/api.ts Normal file
View File

@ -0,0 +1,181 @@
import { Connection, PublicKey } from '@solana/web3.js'
import * as bs58 from 'bs58'
import { deserializeBorsh } from '../utils/borsh'
import { GOVERNANCE_SCHEMA, ParsedAccount } from './serialisation'
import {
GovernanceAccount,
GovernanceAccountClass,
GovernanceAccountType,
Realm,
} from './accounts'
import { WalletNotConnectedError } from './errors'
export interface IWallet {
publicKey: PublicKey
}
// Context to make RPC calls for given clone programId, current connection, endpoint and wallet
export class RpcContext {
programId: PublicKey
wallet: IWallet | undefined
connection: Connection
endpoint: string
constructor(
programId: PublicKey,
wallet: IWallet | undefined,
connection: Connection,
endpoint: string
) {
this.programId = programId
this.wallet = wallet
this.connection = connection
this.endpoint = endpoint
}
get walletPubkey() {
if (!this.wallet?.publicKey) {
throw new WalletNotConnectedError()
}
return this.wallet.publicKey
}
get programIdBase58() {
return this.programId.toBase58()
}
}
export class MemcmpFilter {
offset: number
bytes: Buffer
constructor(offset: number, bytes: Buffer) {
this.offset = offset
this.bytes = bytes
}
isMatch(buffer: Buffer) {
if (this.offset + this.bytes.length > buffer.length) {
return false
}
for (let i = 0; i < this.bytes.length; i++) {
if (this.bytes[i] !== buffer[this.offset + i]) return false
}
return true
}
}
export const pubkeyFilter = (
offset: number,
pubkey: PublicKey | undefined | null
) => (!pubkey ? undefined : new MemcmpFilter(offset, pubkey.toBuffer()))
export async function getRealms(rpcContext: RpcContext) {
return getGovernanceAccountsImpl<Realm>(
rpcContext.programId,
rpcContext.endpoint,
Realm,
GovernanceAccountType.Realm
)
}
export async function getGovernanceAccounts<TAccount extends GovernanceAccount>(
programId: PublicKey,
endpoint: string,
accountClass: GovernanceAccountClass,
accountTypes: GovernanceAccountType[],
filters: MemcmpFilter[] = []
) {
if (accountTypes.length === 1) {
return getGovernanceAccountsImpl<TAccount>(
programId,
endpoint,
accountClass,
accountTypes[0],
filters
)
}
const all = await Promise.all(
accountTypes.map((at) =>
getGovernanceAccountsImpl<TAccount>(
programId,
endpoint,
accountClass,
at,
filters
)
)
)
return all.reduce((res, r) => ({ ...res, ...r }), {}) as Record<
string,
ParsedAccount<TAccount>
>
}
async function getGovernanceAccountsImpl<TAccount extends GovernanceAccount>(
programId: PublicKey,
endpoint: string,
accountClass: GovernanceAccountClass,
accountType: GovernanceAccountType,
filters: MemcmpFilter[] = []
) {
const getProgramAccounts = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getProgramAccounts',
params: [
programId.toBase58(),
{
commitment: 'single',
encoding: 'base64',
filters: [
{
memcmp: {
offset: 0,
bytes: bs58.encode([accountType]),
},
},
...filters.map((f) => ({
memcmp: { offset: f.offset, bytes: bs58.encode(f.bytes) },
})),
],
},
],
}),
})
const rawAccounts = (await getProgramAccounts.json())['result']
const accounts: Record<string, ParsedAccount<TAccount>> = {}
for (const rawAccount of rawAccounts) {
try {
const account = {
pubkey: new PublicKey(rawAccount.pubkey),
account: {
...rawAccount.account,
data: [], // There is no need to keep the raw data around once we deserialize it into TAccount
},
info: deserializeBorsh(
GOVERNANCE_SCHEMA,
accountClass,
Buffer.from(rawAccount.account.data[0], 'base64')
),
}
accounts[account.pubkey.toBase58()] = account
} catch (ex) {
console.error(`Can't deserialize ${accountClass}`, ex)
}
}
return accounts
}

View File

@ -0,0 +1,28 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js'
import { GovernanceConfig } from './accounts'
import { SetGovernanceConfigArgs } from './instructions'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
export function createSetGovernanceConfig(
programId: PublicKey,
governance: PublicKey,
governanceConfig: GovernanceConfig
) {
const args = new SetGovernanceConfigArgs({ config: governanceConfig })
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const keys = [
{
pubkey: governance,
isWritable: true,
isSigner: true,
},
]
return new TransactionInstruction({
keys,
programId,
data,
})
}

View File

@ -0,0 +1,71 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js'
import {
getTokenHoldingAddress,
MintMaxVoteWeightSource,
RealmConfigArgs,
} from './accounts'
import { SetRealmConfigArgs } from './instructions'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import BN from 'bn.js'
export async function createSetRealmConfig(
programId: PublicKey,
realm: PublicKey,
realmAuthority: PublicKey,
councilMint: PublicKey | undefined,
communityMintMaxVoteWeightSource: MintMaxVoteWeightSource,
minCommunityTokensToCreateGovernance: BN
) {
const configArgs = new RealmConfigArgs({
useCouncilMint: councilMint !== undefined,
communityMintMaxVoteWeightSource,
minCommunityTokensToCreateGovernance,
})
const args = new SetRealmConfigArgs({ configArgs })
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
let keys = [
{
pubkey: realm,
isWritable: true,
isSigner: false,
},
{
pubkey: realmAuthority,
isWritable: false,
isSigner: true,
},
]
if (councilMint) {
const councilTokenHoldingAddress = await getTokenHoldingAddress(
programId,
realm,
councilMint
)
keys = [
...keys,
{
pubkey: councilMint,
isSigner: false,
isWritable: false,
},
{
pubkey: councilTokenHoldingAddress,
isSigner: false,
isWritable: true,
},
]
}
return new TransactionInstruction({
keys,
programId,
data,
})
}

11
models/enums.ts Normal file
View File

@ -0,0 +1,11 @@
export enum GoverningTokenType {
Community,
Council,
}
export enum GovernanceType {
Account,
Program,
Mint,
Token,
}

146
models/errors.ts Normal file
View File

@ -0,0 +1,146 @@
import { TransactionError } from '@solana/web3.js'
export class SendTransactionError extends Error {
txError: TransactionError
txId: string
constructor(message: string, txId: string, txError: TransactionError) {
super(message)
this.txError = txError
this.txId = txId
}
}
export const GovernanceError: Record<number, string> = [
'Invalid instruction passed to program', // InvalidInstruction
'Realm with the given name and governing mints already exists', // RealmAlreadyExists
'Invalid realm', // InvalidRealm
'Invalid Governing Token Mint', // InvalidGoverningTokenMint
'Governing Token Owner must sign transaction', // GoverningTokenOwnerMustSign
'Governing Token Owner or Delegate must sign transaction', // GoverningTokenOwnerOrDelegateMustSign
'All votes must be relinquished to withdraw governing tokens', // AllVotesMustBeRelinquishedToWithdrawGoverningTokens
'Invalid Token Owner Record account address', // InvalidTokenOwnerRecordAccountAddress
'Invalid GoverningMint for TokenOwnerRecord', // InvalidGoverningMintForTokenOwnerRecord
'Invalid Realm for TokenOwnerRecord', // InvalidRealmForTokenOwnerRecord
'Invalid Proposal for ProposalInstruction', // InvalidProposalForProposalInstruction
'Invalid Signatory account address', // InvalidSignatoryAddress
'Signatory already signed off', // SignatoryAlreadySignedOff
'Signatory must sign', // SignatoryMustSign
'Invalid Proposal Owner', //InvalidProposalOwnerAccount
'Invalid Proposal for VoterRecord', // InvalidProposalForVoterRecord
'Invalid GoverningTokenOwner for VoteRecord', // InvalidGoverningTokenOwnerForVoteRecord
'Invalid Governance config: Vote threshold percentage out of range', // InvalidVoteThresholdPercentage
'Proposal for the given Governance, Governing Token Mint and index already exists', // ProposalAlreadyExists
'Token Owner already voted on the Proposal', // VoteAlreadyExists
"Owner doesn't have enough governing tokens to create Proposal", // NotEnoughTokensToCreateProposal
"Invalid State: Can't edit Signatories", // InvalidStateCannotEditSignatories
'Invalid Proposal state', // InvalidProposalState
"Invalid State: Can't edit instructions", // InvalidStateCannotEditInstructions
"Invalid State: Can't execute instruction", // InvalidStateCannotExecuteInstruction
"Can't execute instruction within its hold up time", // CannotExecuteInstructionWithinHoldUpTime
'Instruction already executed', // InstructionAlreadyExecuted
'Invalid Instruction index', // InvalidInstructionIndex
'Instruction hold up time is below the min specified by Governance', // InstructionHoldUpTimeBelowRequiredMin
'Instruction at the given index for the Proposal already exists', // InstructionAlreadyExists
"Invalid State: Can't sign off", // InvalidStateCannotSignOff
"Invalid State: Can't vote", // InvalidStateCannotVote
"Invalid State: Can't finalize vote", // InvalidStateCannotFinalize
"Invalid State: Can't cancel Proposal", // InvalidStateCannotCancelProposal
'Vote already relinquished', // VoteAlreadyRelinquished
"Can't finalize vote. Voting still in progress", // CannotFinalizeVotingInProgress
'Proposal voting time expired', // ProposalVotingTimeExpired
'Invalid Signatory Mint', // InvalidSignatoryMint
'Invalid account owner', // InvalidAccountOwner
"Account doesn't exist", // AccountDoesNotExist
'Invalid Account type', // InvalidAccountType
'Proposal does not belong to the given Governance', // InvalidGovernanceForProposal
'Proposal does not belong to given Governing Mint', // InvalidGoverningMintForProposal
'Current mint authority must sign transaction', // MintAuthorityMustSign
'Invalid mint authority', // InvalidMintAuthority
'Mint has no authority', // MintHasNoAuthority
'Invalid Token account owner', // SplTokenAccountWithInvalidOwner
'Invalid Mint account owner', // SplTokenMintWithInvalidOwner
'Token Account is not initialized', // SplTokenAccountNotInitialized
"Token Account doesn't exist", // SplTokenAccountDoesNotExist
'Token account data is invalid', // SplTokenInvalidTokenAccountData
'Token mint account data is invalid', // SplTokenInvalidMintAccountData
'Token Mint account is not initialized', // SplTokenMintNotInitialized
"Token Mint account doesn't exist", // SplTokenMintDoesNotExist
'Invalid ProgramData account address', // InvalidProgramDataAccountAddress
'Invalid ProgramData account Data', // InvalidProgramDataAccountData
"Provided upgrade authority doesn't match current program upgrade authority", // InvalidUpgradeAuthority
'Current program upgrade authority must sign transaction', // UpgradeAuthorityMustSign
'Given program is not upgradable', //ProgramNotUpgradable
'Invalid token owner', //InvalidTokenOwner
'Current token owner must sign transaction', // TokenOwnerMustSign
'Given VoteThresholdPercentageType is not supported', //VoteThresholdPercentageTypeNotSupported
'Given VoteWeightSource is not supported', //VoteWeightSourceNotSupported
'Proposal cool off time is not supported', // ProposalCoolOffTimeNotSupported
'Governance PDA must sign', // GovernancePdaMustSign
'Instruction already flagged with error', // InstructionAlreadyFlaggedWithError
'Invalid Realm for Governance', // InvalidRealmForGovernance
'Invalid Authority for Realm', // InvalidAuthorityForRealm
'Realm has no authority', // RealmHasNoAuthority
'Realm authority must sign', // RealmAuthorityMustSign
'Invalid governing token holding account', // InvalidGoverningTokenHoldingAccount
'Realm council mint change is not supported', // RealmCouncilMintChangeIsNotSupported
'Not supported mint max vote weight source', // MintMaxVoteWeightSourceNotSupported
'Invalid max vote weight supply fraction', // InvalidMaxVoteWeightSupplyFraction
"Owner doesn't have enough governing tokens to create Governance", // NotEnoughTokensToCreateGovernance
]
export const TokenError: Record<number, string> = [
'Lamport balance below rent-exempt threshold', // NotRentExempt
'Insufficient funds', // InsufficientFunds
'Invalid Mint', // InvalidMint
'Account not associated with this Mint', // MintMismatch,
'Owner does not match', // OwnerMismatch,
'Fixed supply', // FixedSupply,
'Already in use', // AlreadyInUse,
'Invalid number of provided signers', // InvalidNumberOfProvidedSigners,
'Invalid number of required signers', // InvalidNumberOfRequiredSigners,
'State is uninitialized', // UninitializedState,
'Instruction does not support native tokens', // NativeNotSupported,
'Non-native account can only be closed if its balance is zero', // NonNativeHasBalance,
'Invalid instruction', // InvalidInstruction,
'State is invalid for requested operation', // InvalidState,
'Operation overflowed', // Overflow,
'Account does not support specified authority type', // AuthorityTypeNotSupported,
'This token mint cannot freeze accounts', // MintCannotFreeze,
'Account is frozen', // AccountFrozen,
'The provided decimals value different from the Mint decimals', // MintDecimalsMismatch,
]
const governanceErrorOffset = 500
export function getTransactionErrorMsg(error: SendTransactionError) {
try {
const instructionError = (error.txError as any).InstructionError[1]
if (instructionError.Custom !== undefined) {
if (instructionError.Custom >= governanceErrorOffset) {
return GovernanceError[instructionError.Custom - governanceErrorOffset]
} else {
// If the error is not from the Governance error space then it's ambiguous because the custom errors share the same space
// And we can only use some heuristics here to guess what program returned the error
// For now the most common scenario is an error returned from the token program so I'm mapping the custom errors to it with the 'possible' warning
return `Possible error: ${TokenError[instructionError.Custom]}`
}
} else {
return instructionError
}
} catch {
return JSON.stringify(error)
}
}
export class WalletNotConnectedError extends Error {
constructor() {
super('Wallet is not connected.')
}
}
export function isWalletNotConnectedError(
error: any
): error is WalletNotConnectedError {
return error instanceof WalletNotConnectedError
}

218
models/instructions.ts Normal file
View File

@ -0,0 +1,218 @@
import { PublicKey } from '@solana/web3.js'
import { RealmConfigArgs, GovernanceConfig, InstructionData } from './accounts'
export enum GovernanceInstruction {
CreateRealm = 0,
DepositGoverningTokens = 1,
WithdrawGoverningTokens = 2,
SetGovernanceDelegate = 3, // --
CreateAccountGovernance = 4,
CreateProgramGovernance = 5,
CreateProposal = 6,
AddSignatory = 7,
RemoveSignatory = 8,
InsertInstruction = 9,
RemoveInstruction = 10,
CancelProposal = 11,
SignOffProposal = 12,
CastVote = 13,
FinalizeVote = 14,
RelinquishVote = 15,
ExecuteInstruction = 16,
CreateMintGovernance = 17,
CreateTokenGovernance = 18,
SetGovernanceConfig = 19,
FlagInstructionError = 20,
SetRealmAuthority = 21,
SetRealmConfig = 22,
}
export class CreateRealmArgs {
instruction: GovernanceInstruction = GovernanceInstruction.CreateRealm
configArgs: RealmConfigArgs
name: string
constructor(args: { name: string; configArgs: RealmConfigArgs }) {
this.name = args.name
this.configArgs = args.configArgs
}
}
export class DepositGoverningTokensArgs {
instruction: GovernanceInstruction =
GovernanceInstruction.DepositGoverningTokens
}
export class WithdrawGoverningTokensArgs {
instruction: GovernanceInstruction =
GovernanceInstruction.WithdrawGoverningTokens
}
export class CreateAccountGovernanceArgs {
instruction: GovernanceInstruction =
GovernanceInstruction.CreateAccountGovernance
config: GovernanceConfig
constructor(args: { config: GovernanceConfig }) {
this.config = args.config
}
}
export class CreateProgramGovernanceArgs {
instruction: GovernanceInstruction =
GovernanceInstruction.CreateProgramGovernance
config: GovernanceConfig
transferUpgradeAuthority: boolean
constructor(args: {
config: GovernanceConfig
transferUpgradeAuthority: boolean
}) {
this.config = args.config
this.transferUpgradeAuthority = !!args.transferUpgradeAuthority
}
}
export class CreateMintGovernanceArgs {
instruction: GovernanceInstruction =
GovernanceInstruction.CreateMintGovernance
config: GovernanceConfig
transferMintAuthority: boolean
constructor(args: {
config: GovernanceConfig
transferMintAuthority: boolean
}) {
this.config = args.config
this.transferMintAuthority = !!args.transferMintAuthority
}
}
export class CreateTokenGovernanceArgs {
instruction: GovernanceInstruction =
GovernanceInstruction.CreateTokenGovernance
config: GovernanceConfig
transferTokenOwner: boolean
constructor(args: { config: GovernanceConfig; transferTokenOwner: boolean }) {
this.config = args.config
this.transferTokenOwner = !!args.transferTokenOwner
}
}
export class SetGovernanceConfigArgs {
instruction: GovernanceInstruction = GovernanceInstruction.SetGovernanceConfig
config: GovernanceConfig
constructor(args: { config: GovernanceConfig }) {
this.config = args.config
}
}
export class CreateProposalArgs {
instruction: GovernanceInstruction = GovernanceInstruction.CreateProposal
name: string
descriptionLink: string
governingTokenMint: PublicKey
constructor(args: {
name: string
descriptionLink: string
governingTokenMint: PublicKey
}) {
this.name = args.name
this.descriptionLink = args.descriptionLink
this.governingTokenMint = args.governingTokenMint
}
}
export class AddSignatoryArgs {
instruction: GovernanceInstruction = GovernanceInstruction.AddSignatory
signatory: PublicKey
constructor(args: { signatory: PublicKey }) {
this.signatory = args.signatory
}
}
export class SignOffProposalArgs {
instruction: GovernanceInstruction = GovernanceInstruction.SignOffProposal
}
export class CancelProposalArgs {
instruction: GovernanceInstruction = GovernanceInstruction.CancelProposal
}
export enum Vote {
Yes,
No,
}
export class CastVoteArgs {
instruction: GovernanceInstruction = GovernanceInstruction.CastVote
vote: Vote
constructor(args: { vote: Vote }) {
this.vote = args.vote
}
}
export class RelinquishVoteArgs {
instruction: GovernanceInstruction = GovernanceInstruction.RelinquishVote
}
export class FinalizeVoteArgs {
instruction: GovernanceInstruction = GovernanceInstruction.FinalizeVote
}
export class InsertInstructionArgs {
instruction: GovernanceInstruction = GovernanceInstruction.InsertInstruction
index: number
holdUpTime: number
instructionData: InstructionData
constructor(args: {
index: number
holdUpTime: number
instructionData: InstructionData
}) {
this.index = args.index
this.holdUpTime = args.holdUpTime
this.instructionData = args.instructionData
}
}
export class RemoveInstructionArgs {
instruction: GovernanceInstruction = GovernanceInstruction.RemoveInstruction
}
export class ExecuteInstructionArgs {
instruction: GovernanceInstruction = GovernanceInstruction.ExecuteInstruction
}
export class FlagInstructionErrorArgs {
instruction: GovernanceInstruction =
GovernanceInstruction.FlagInstructionError
}
export class SetRealmAuthorityArgs {
instruction: GovernanceInstruction = GovernanceInstruction.SetRealmAuthority
newRealmAuthority: PublicKey
constructor(args: { newRealmAuthority: PublicKey }) {
this.newRealmAuthority = args.newRealmAuthority
}
}
export class SetRealmConfigArgs {
instruction: GovernanceInstruction = GovernanceInstruction.SetRealmConfig
configArgs: RealmConfigArgs
constructor(args: { configArgs: RealmConfigArgs }) {
this.configArgs = args.configArgs
}
}

525
models/serialisation.ts Normal file
View File

@ -0,0 +1,525 @@
import { AccountInfo, PublicKey, TransactionInstruction } from '@solana/web3.js'
import { deserializeBorsh } from '../utils/borsh'
import { BinaryReader, BinaryWriter } from 'borsh'
import {
AddSignatoryArgs,
CancelProposalArgs,
CastVoteArgs,
CreateAccountGovernanceArgs,
CreateMintGovernanceArgs,
CreateProgramGovernanceArgs,
CreateProposalArgs,
CreateRealmArgs,
CreateTokenGovernanceArgs,
DepositGoverningTokensArgs,
ExecuteInstructionArgs,
FinalizeVoteArgs,
FlagInstructionErrorArgs,
InsertInstructionArgs,
RelinquishVoteArgs,
RemoveInstructionArgs,
SetGovernanceConfigArgs,
SetRealmAuthorityArgs,
SetRealmConfigArgs,
SignOffProposalArgs,
WithdrawGoverningTokensArgs,
} from './instructions'
import {
AccountMetaData,
RealmConfigArgs,
Governance,
GovernanceConfig,
InstructionData,
MintMaxVoteWeightSource,
Proposal,
ProposalInstruction,
Realm,
RealmConfig,
SignatoryRecord,
TokenOwnerRecord,
VoteRecord,
VoteThresholdPercentage,
VoteWeight,
} from './accounts'
import { serialize } from 'borsh'
// Temp. workaround to support u16.
;(BinaryReader.prototype as any).readU16 = function () {
const reader = (this as unknown) as BinaryReader
const value = reader.buf.readUInt16LE(reader.offset)
reader.offset += 2
return value
}
// Temp. workaround to support u16.
;(BinaryWriter.prototype as any).writeU16 = function (value: number) {
const reader = (this as unknown) as BinaryWriter
reader.maybeResize()
reader.buf.writeUInt16LE(value, reader.length)
reader.length += 2
}
// Serializes sdk instruction into InstructionData and encodes it as base64 which then can be entered into the UI form
export const serializeInstructionToBase64 = (
instruction: TransactionInstruction
) => {
const data = new InstructionData({
programId: instruction.programId,
data: instruction.data,
accounts: instruction.keys.map(
(k) =>
new AccountMetaData({
pubkey: k.pubkey,
isSigner: k.isSigner,
isWritable: k.isWritable,
})
),
})
return Buffer.from(serialize(GOVERNANCE_SCHEMA, data)).toString('base64')
}
export const GOVERNANCE_SCHEMA = new Map<any, any>([
[
RealmConfigArgs,
{
kind: 'struct',
fields: [
['useCouncilMint', 'u8'],
['minCommunityTokensToCreateGovernance', 'u64'],
['communityMintMaxVoteWeightSource', MintMaxVoteWeightSource],
],
},
],
[
CreateRealmArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['name', 'string'],
['configArgs', RealmConfigArgs],
],
},
],
[
DepositGoverningTokensArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
WithdrawGoverningTokensArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
CreateAccountGovernanceArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['config', GovernanceConfig],
],
},
],
[
CreateProgramGovernanceArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['config', GovernanceConfig],
['transferUpgradeAuthority', 'u8'],
],
},
],
[
CreateMintGovernanceArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['config', GovernanceConfig],
['transferMintAuthority', 'u8'],
],
},
],
[
CreateTokenGovernanceArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['config', GovernanceConfig],
['transferTokenOwner', 'u8'],
],
},
],
[
SetGovernanceConfigArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['config', GovernanceConfig],
],
},
],
[
CreateProposalArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['name', 'string'],
['descriptionLink', 'string'],
['governingTokenMint', 'pubkey'],
],
},
],
[
AddSignatoryArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['signatory', 'pubkey'],
],
},
],
[
SignOffProposalArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
CancelProposalArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
RelinquishVoteArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
FinalizeVoteArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
CastVoteArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['vote', 'u8'],
],
},
],
[
InsertInstructionArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['index', 'u16'],
['holdUpTime', 'u32'],
['instructionData', InstructionData],
],
},
],
[
RemoveInstructionArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
ExecuteInstructionArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
FlagInstructionErrorArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
SetRealmAuthorityArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['newRealmAuthority', { kind: 'option', type: 'pubkey' }],
],
},
],
[
SetRealmConfigArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['configArgs', RealmConfigArgs],
],
},
],
[
InstructionData,
{
kind: 'struct',
fields: [
['programId', 'pubkey'],
['accounts', [AccountMetaData]],
['data', ['u8']],
],
},
],
[
AccountMetaData,
{
kind: 'struct',
fields: [
['pubkey', 'pubkey'],
['isSigner', 'u8'],
['isWritable', 'u8'],
],
},
],
[
MintMaxVoteWeightSource,
{
kind: 'struct',
fields: [
['type', 'u8'],
['value', 'u64'],
],
},
],
[
RealmConfig,
{
kind: 'struct',
fields: [
['reserved', [8]],
['minCommunityTokensToCreateGovernance', 'u64'],
['communityMintMaxVoteWeightSource', MintMaxVoteWeightSource],
['councilMint', { kind: 'option', type: 'pubkey' }],
],
},
],
[
Realm,
{
kind: 'struct',
fields: [
['accountType', 'u8'],
['communityMint', 'pubkey'],
['config', RealmConfig],
['reserved', [8]],
['authority', { kind: 'option', type: 'pubkey' }],
['name', 'string'],
],
},
],
[
Governance,
{
kind: 'struct',
fields: [
['accountType', 'u8'],
['realm', 'pubkey'],
['governedAccount', 'pubkey'],
['proposalCount', 'u32'],
['config', GovernanceConfig],
['reserved', [8]],
],
},
],
[
VoteThresholdPercentage,
{
kind: 'struct',
fields: [
['type', 'u8'],
['value', 'u8'],
],
},
],
[
GovernanceConfig,
{
kind: 'struct',
fields: [
['voteThresholdPercentage', VoteThresholdPercentage],
['minCommunityTokensToCreateProposal', 'u64'],
['minInstructionHoldUpTime', 'u32'],
['maxVotingTime', 'u32'],
['voteWeightSource', 'u8'],
['proposalCoolOffTime', 'u32'],
['minCouncilTokensToCreateProposal', 'u64'],
],
},
],
[
TokenOwnerRecord,
{
kind: 'struct',
fields: [
['accountType', 'u8'],
['realm', 'pubkey'],
['governingTokenMint', 'pubkey'],
['governingTokenOwner', 'pubkey'],
['governingTokenDepositAmount', 'u64'],
['unrelinquishedVotesCount', 'u32'],
['totalVotesCount', 'u32'],
['reserved', [8]],
['governanceDelegate', { kind: 'option', type: 'pubkey' }],
],
},
],
[
Proposal,
{
kind: 'struct',
fields: [
['accountType', 'u8'],
['governance', 'pubkey'],
['governingTokenMint', 'pubkey'],
['state', 'u8'],
['tokenOwnerRecord', 'pubkey'],
['signatoriesCount', 'u8'],
['signatoriesSignedOffCount', 'u8'],
['yesVotesCount', 'u64'],
['noVotesCount', 'u64'],
['instructionsExecutedCount', 'u16'],
['instructionsCount', 'u16'],
['instructionsNextIndex', 'u16'],
['draftAt', 'u64'],
['signingOffAt', { kind: 'option', type: 'u64' }],
['votingAt', { kind: 'option', type: 'u64' }],
['votingAtSlot', { kind: 'option', type: 'u64' }],
['votingCompletedAt', { kind: 'option', type: 'u64' }],
['executingAt', { kind: 'option', type: 'u64' }],
['closedAt', { kind: 'option', type: 'u64' }],
['executionFlags', 'u8'],
['maxVoteWeight', { kind: 'option', type: 'u64' }],
[
'voteThresholdPercentage',
{ kind: 'option', type: VoteThresholdPercentage },
],
['name', 'string'],
['descriptionLink', 'string'],
],
},
],
[
SignatoryRecord,
{
kind: 'struct',
fields: [
['accountType', 'u8'],
['proposal', 'pubkey'],
['signatory', 'pubkey'],
['signedOff', 'u8'],
],
},
],
[
VoteWeight,
{
kind: 'enum',
values: [
['yes', 'u64'],
['no', 'u64'],
],
},
],
[
VoteRecord,
{
kind: 'struct',
fields: [
['accountType', 'u8'],
['proposal', 'pubkey'],
['governingTokenOwner', 'pubkey'],
['isRelinquished', 'u8'],
['voteWeight', VoteWeight],
],
},
],
[
ProposalInstruction,
{
kind: 'struct',
fields: [
['accountType', 'u8'],
['proposal', 'pubkey'],
['instructionIndex', 'u16'],
['holdUpTime', 'u32'],
['instruction', InstructionData],
['executedAt', { kind: 'option', type: 'u64' }],
['executionStatus', 'u8'],
],
},
],
])
export interface ParsedAccountBase {
pubkey: PublicKey
account: AccountInfo<Buffer>
info: unknown
}
export interface ParsedAccount<T> extends ParsedAccountBase {
info: T
}
export function BorshAccountParser(
classType: any
): (pubKey: PublicKey, info: AccountInfo<Buffer>) => ParsedAccountBase {
return (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
const buffer = Buffer.from(info.data)
const data = deserializeBorsh(GOVERNANCE_SCHEMA, classType, buffer)
return {
pubkey: pubKey,
account: {
...info,
},
info: data,
} as ParsedAccountBase
}
}
export function getInstructionDataFromBase64(instructionDataBase64: string) {
const instructionDataBin = Buffer.from(instructionDataBase64, 'base64')
const instructionData: InstructionData = deserializeBorsh(
GOVERNANCE_SCHEMA,
InstructionData,
instructionDataBin
)
return instructionData
}

View File

@ -0,0 +1,75 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { AddSignatoryArgs } from './instructions'
import { getSignatoryRecordAddress } from './accounts'
export const withAddSignatory = async (
instructions: TransactionInstruction[],
programId: PublicKey,
proposal: PublicKey,
tokenOwnerRecord: PublicKey,
governanceAuthority: PublicKey,
signatory: PublicKey,
payer: PublicKey,
systemId: PublicKey
) => {
const args = new AddSignatoryArgs({ signatory })
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const signatoryRecordAddress = await getSignatoryRecordAddress(
programId,
proposal,
signatory
)
const keys = [
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: governanceAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: signatoryRecordAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: systemId,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,50 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { CancelProposalArgs } from './instructions'
export const withCancelProposal = (
instructions: TransactionInstruction[],
programId: PublicKey,
proposal: PublicKey,
tokenOwnerRecord: PublicKey,
governanceAuthority: PublicKey
) => {
const args = new CancelProposalArgs()
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const keys = [
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: governanceAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isSigner: false,
isWritable: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

102
models/withCastVote.ts Normal file
View File

@ -0,0 +1,102 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { CastVoteArgs, Vote } from './instructions'
import { GOVERNANCE_PROGRAM_SEED } from './accounts'
export const withCastVote = async (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
governance: PublicKey,
proposal: PublicKey,
tokenOwnerRecord: PublicKey,
governanceAuthority: PublicKey,
governingTokenMint: PublicKey,
vote: Vote,
payer: PublicKey,
systemId: PublicKey
) => {
const args = new CastVoteArgs({ vote })
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const [voteRecordAddress] = await PublicKey.findProgramAddress(
[
Buffer.from(GOVERNANCE_PROGRAM_SEED),
proposal.toBuffer(),
tokenOwnerRecord.toBuffer(),
],
programId
)
const keys = [
{
pubkey: realm,
isWritable: false,
isSigner: false,
},
{
pubkey: governance,
isWritable: false,
isSigner: false,
},
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwnerRecord,
isWritable: true,
isSigner: false,
},
{
pubkey: governanceAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: voteRecordAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: governingTokenMint,
isWritable: false,
isSigner: false,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: systemId,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isSigner: false,
isWritable: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,80 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { GovernanceConfig } from './accounts'
import { CreateAccountGovernanceArgs } from './instructions'
export const withCreateAccountGovernance = async (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
governedAccount: PublicKey,
config: GovernanceConfig,
tokenOwnerRecord: PublicKey,
payer: PublicKey,
systemId: PublicKey
): Promise<{ governanceAddress: PublicKey }> => {
const args = new CreateAccountGovernanceArgs({ config })
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const [governanceAddress] = await PublicKey.findProgramAddress(
[
Buffer.from('account-governance'),
realm.toBuffer(),
governedAccount.toBuffer(),
],
programId
)
const keys = [
{
pubkey: realm,
isWritable: false,
isSigner: false,
},
{
pubkey: governanceAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: governedAccount,
isWritable: false,
isSigner: false,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: systemId,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
return { governanceAddress }
}

View File

@ -0,0 +1,92 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { GovernanceConfig } from './accounts'
import { CreateMintGovernanceArgs } from './instructions'
export const withCreateMintGovernance = async (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
governedMint: PublicKey,
config: GovernanceConfig,
transferMintAuthority: boolean,
mintAuthority: PublicKey,
tokenOwnerRecord: PublicKey,
payer: PublicKey,
tokenId: PublicKey,
systemId: PublicKey
): Promise<{ governanceAddress: PublicKey }> => {
const args = new CreateMintGovernanceArgs({
config,
transferMintAuthority,
})
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const [mintGovernanceAddress] = await PublicKey.findProgramAddress(
[Buffer.from('mint-governance'), realm.toBuffer(), governedMint.toBuffer()],
programId
)
const keys = [
{
pubkey: realm,
isWritable: false,
isSigner: false,
},
{
pubkey: mintGovernanceAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: governedMint,
isWritable: true,
isSigner: false,
},
{
pubkey: mintAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: tokenId,
isWritable: false,
isSigner: false,
},
{
pubkey: systemId,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
return { governanceAddress: mintGovernanceAddress }
}

View File

@ -0,0 +1,106 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { GovernanceConfig } from './accounts'
import { CreateProgramGovernanceArgs } from './instructions'
export const withCreateProgramGovernance = async (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
governedProgram: PublicKey,
config: GovernanceConfig,
transferUpgradeAuthority: boolean,
programUpgradeAuthority: PublicKey,
tokenOwnerRecord: PublicKey,
payer: PublicKey,
systemId: PublicKey,
bpfUpgradableLoaderId: PublicKey
): Promise<{ governanceAddress: PublicKey }> => {
const args = new CreateProgramGovernanceArgs({
config,
transferUpgradeAuthority,
})
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const [governanceAddress] = await PublicKey.findProgramAddress(
[
Buffer.from('program-governance'),
realm.toBuffer(),
governedProgram.toBuffer(),
],
programId
)
const [programDataAddress] = await PublicKey.findProgramAddress(
[governedProgram.toBuffer()],
bpfUpgradableLoaderId
)
const keys = [
{
pubkey: realm,
isWritable: false,
isSigner: false,
},
{
pubkey: governanceAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: governedProgram,
isWritable: false,
isSigner: false,
},
{
pubkey: programDataAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: programUpgradeAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: bpfUpgradableLoaderId,
isWritable: false,
isSigner: false,
},
{
pubkey: systemId,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
return { governanceAddress }
}

View File

@ -0,0 +1,103 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { CreateProposalArgs } from './instructions'
import { GOVERNANCE_PROGRAM_SEED } from './accounts'
export const withCreateProposal = async (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
governance: PublicKey,
tokenOwnerRecord: PublicKey,
name: string,
descriptionLink: string,
governingTokenMint: PublicKey,
governanceAuthority: PublicKey,
proposalIndex: number,
payer: PublicKey,
systemId: PublicKey
) => {
const args = new CreateProposalArgs({
name,
descriptionLink,
governingTokenMint,
})
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const proposalIndexBuffer = Buffer.alloc(4)
proposalIndexBuffer.writeInt32LE(proposalIndex, 0)
const [proposalAddress] = await PublicKey.findProgramAddress(
[
Buffer.from(GOVERNANCE_PROGRAM_SEED),
governance.toBuffer(),
governingTokenMint.toBuffer(),
proposalIndexBuffer,
],
programId
)
const keys = [
{
pubkey: realm,
isWritable: false,
isSigner: false,
},
{
pubkey: proposalAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: governance,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: governanceAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: systemId,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isWritable: false,
isSigner: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
return proposalAddress
}

127
models/withCreateRealm.ts Normal file
View File

@ -0,0 +1,127 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { CreateRealmArgs } from './instructions'
import {
RealmConfigArgs,
GOVERNANCE_PROGRAM_SEED,
MintMaxVoteWeightSource,
getTokenHoldingAddress,
} from './accounts'
import BN from 'bn.js'
export async function withCreateRealm(
instructions: TransactionInstruction[],
programId: PublicKey,
name: string,
realmAuthority: PublicKey,
communityMint: PublicKey,
payer: PublicKey,
councilMint: PublicKey | undefined,
communityMintMaxVoteWeightSource: MintMaxVoteWeightSource,
minCommunityTokensToCreateGovernance: BN,
systemId: PublicKey,
tokenId: PublicKey
) {
const configArgs = new RealmConfigArgs({
useCouncilMint: councilMint !== undefined,
minCommunityTokensToCreateGovernance,
communityMintMaxVoteWeightSource,
})
const args = new CreateRealmArgs({
configArgs,
name,
})
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const [realmAddress] = await PublicKey.findProgramAddress(
[Buffer.from(GOVERNANCE_PROGRAM_SEED), Buffer.from(args.name)],
programId
)
const communityTokenHoldingAddress = await getTokenHoldingAddress(
programId,
realmAddress,
communityMint
)
let keys = [
{
pubkey: realmAddress,
isSigner: false,
isWritable: true,
},
{
pubkey: realmAuthority,
isSigner: false,
isWritable: false,
},
{
pubkey: communityMint,
isSigner: false,
isWritable: false,
},
{
pubkey: communityTokenHoldingAddress,
isSigner: false,
isWritable: true,
},
{
pubkey: payer,
isSigner: true,
isWritable: false,
},
{
pubkey: systemId,
isSigner: false,
isWritable: false,
},
{
pubkey: tokenId,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
]
if (councilMint) {
const councilTokenHoldingAddress = await getTokenHoldingAddress(
programId,
realmAddress,
councilMint
)
keys = [
...keys,
{
pubkey: councilMint,
isSigner: false,
isWritable: false,
},
{
pubkey: councilTokenHoldingAddress,
isSigner: false,
isWritable: true,
},
]
}
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
return realmAddress
}

View File

@ -0,0 +1,34 @@
import {
Account,
Connection,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js'
import * as serum from '@project-serum/common'
import { IWallet } from './api'
export const withCreateSplTokenAccount = async (
connection: Connection,
wallet: IWallet | undefined,
instructions: TransactionInstruction[],
signers: Account[],
mint: PublicKey
): Promise<{ tokenAccountAddress: PublicKey }> => {
const tokenAccount = new Account()
const provider = new serum.Provider(
connection,
wallet as serum.Wallet,
serum.Provider.defaultOptions()
)
instructions.push(
...(await serum.createTokenAccountInstrs(
provider,
tokenAccount.publicKey,
mint,
wallet!.publicKey!
))
)
signers.push(tokenAccount)
return { tokenAccountAddress: tokenAccount.publicKey }
}

View File

@ -0,0 +1,96 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { GovernanceConfig } from './accounts'
import { CreateTokenGovernanceArgs } from './instructions'
export const withCreateTokenGovernance = async (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
governedToken: PublicKey,
config: GovernanceConfig,
transferTokenOwner: boolean,
tokenOwner: PublicKey,
tokenOwnerRecord: PublicKey,
payer: PublicKey,
tokenId: PublicKey,
systemId: PublicKey
): Promise<{ governanceAddress: PublicKey }> => {
const args = new CreateTokenGovernanceArgs({
config,
transferTokenOwner,
})
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const [tokenGovernanceAddress] = await PublicKey.findProgramAddress(
[
Buffer.from('token-governance'),
realm.toBuffer(),
governedToken.toBuffer(),
],
programId
)
const keys = [
{
pubkey: realm,
isWritable: false,
isSigner: false,
},
{
pubkey: tokenGovernanceAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: governedToken,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwner,
isWritable: false,
isSigner: true,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: tokenId,
isWritable: false,
isSigner: false,
},
{
pubkey: systemId,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
return { governanceAddress: tokenGovernanceAddress }
}

View File

@ -0,0 +1,102 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { DepositGoverningTokensArgs } from './instructions'
import { getTokenOwnerAddress, GOVERNANCE_PROGRAM_SEED } from './accounts'
export const withDepositGoverningTokens = async (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
governingTokenSource: PublicKey,
governingTokenMint: PublicKey,
governingTokenOwner: PublicKey,
transferAuthority: PublicKey,
payer: PublicKey,
tokenId: PublicKey,
systemId: PublicKey
) => {
const args = new DepositGoverningTokensArgs()
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const tokenOwnerRecordAddress = await getTokenOwnerAddress(
programId,
realm,
governingTokenMint,
governingTokenOwner
)
const [governingTokenHoldingAddress] = await PublicKey.findProgramAddress(
[
Buffer.from(GOVERNANCE_PROGRAM_SEED),
realm.toBuffer(),
governingTokenMint.toBuffer(),
],
programId
)
const keys = [
{
pubkey: realm,
isWritable: false,
isSigner: false,
},
{
pubkey: governingTokenHoldingAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: governingTokenSource,
isWritable: true,
isSigner: false,
},
{
pubkey: governingTokenOwner,
isWritable: false,
isSigner: true,
},
{
pubkey: transferAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: tokenOwnerRecordAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: systemId,
isWritable: false,
isSigner: false,
},
{
pubkey: tokenId,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,70 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { ExecuteInstructionArgs } from './instructions'
import { AccountMetaData, InstructionData } from './accounts'
export const withExecuteInstruction = async (
instructions: TransactionInstruction[],
programId: PublicKey,
governance: PublicKey,
proposal: PublicKey,
instructionAddress: PublicKey,
instruction: InstructionData
) => {
const args = new ExecuteInstructionArgs()
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
// When an instruction needs to be signed by the Governance PDA then its isSigner flag has to be reset on AccountMeta
// because the signature will be required during cpi call invoke_signed() and not when we send ExecuteInstruction
instruction.accounts = instruction.accounts.map((a) =>
a.pubkey.toBase58() === governance.toBase58() && a.isSigner
? new AccountMetaData({
pubkey: a.pubkey,
isWritable: a.isWritable,
isSigner: false,
})
: a
)
const keys = [
{
pubkey: governance,
isWritable: false,
isSigner: false,
},
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: instructionAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isSigner: false,
isWritable: false,
},
{
pubkey: instruction.programId,
isWritable: false,
isSigner: false,
},
...instruction.accounts,
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,56 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { FinalizeVoteArgs } from './instructions'
export const withFinalizeVote = async (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
governance: PublicKey,
proposal: PublicKey,
governingTokenMint: PublicKey
) => {
const args = new FinalizeVoteArgs()
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const keys = [
{
pubkey: realm,
isWritable: false,
isSigner: false,
},
{
pubkey: governance,
isWritable: false,
isSigner: false,
},
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: governingTokenMint,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isSigner: false,
isWritable: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,56 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { FlagInstructionErrorArgs } from './instructions'
export const withFlagInstructionError = (
instructions: TransactionInstruction[],
programId: PublicKey,
proposal: PublicKey,
tokenOwnerRecord: PublicKey,
governanceAuthority: PublicKey,
proposalInstruction: PublicKey
) => {
const args = new FlagInstructionErrorArgs()
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const keys = [
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: governanceAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: proposalInstruction,
isWritable: true,
isSigner: false,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isSigner: false,
isWritable: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,93 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { InsertInstructionArgs } from './instructions'
import { GOVERNANCE_PROGRAM_SEED, InstructionData } from './accounts'
export const withInsertInstruction = async (
instructions: TransactionInstruction[],
programId: PublicKey,
governance: PublicKey,
proposal: PublicKey,
tokenOwnerRecord: PublicKey,
governanceAuthority: PublicKey,
index: number,
holdUpTime: number,
instructionData: InstructionData,
payer: PublicKey,
systemId: PublicKey
) => {
const args = new InsertInstructionArgs({
index,
holdUpTime,
instructionData: instructionData,
})
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const instructionIndexBuffer = Buffer.alloc(2)
instructionIndexBuffer.writeInt16LE(index, 0)
const [proposalInstructionAddress] = await PublicKey.findProgramAddress(
[
Buffer.from(GOVERNANCE_PROGRAM_SEED),
proposal.toBuffer(),
instructionIndexBuffer,
],
programId
)
const keys = [
{
pubkey: governance,
isWritable: false,
isSigner: false,
},
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: governanceAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: proposalInstructionAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: systemId,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,71 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { RelinquishVoteArgs } from './instructions'
export const withRelinquishVote = async (
instructions: TransactionInstruction[],
programId: PublicKey,
governance: PublicKey,
proposal: PublicKey,
tokenOwnerRecord: PublicKey,
governingTokenMint: PublicKey,
voteRecord: PublicKey,
governanceAuthority: PublicKey | undefined,
beneficiary: PublicKey | undefined
) => {
const args = new RelinquishVoteArgs()
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const keys = [
{
pubkey: governance,
isWritable: false,
isSigner: false,
},
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwnerRecord,
isWritable: true,
isSigner: false,
},
{
pubkey: voteRecord,
isWritable: true,
isSigner: false,
},
{
pubkey: governingTokenMint,
isWritable: false,
isSigner: false,
},
]
const existingVoteKeys =
governanceAuthority && beneficiary
? [
{
pubkey: governanceAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: beneficiary,
isWritable: true,
isSigner: false,
},
]
: []
instructions.push(
new TransactionInstruction({
keys: [...keys, ...existingVoteKeys],
programId,
data,
})
)
}

View File

@ -0,0 +1,53 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { RemoveInstructionArgs } from './instructions'
export const withRemoveInstruction = async (
instructions: TransactionInstruction[],
programId: PublicKey,
proposal: PublicKey,
tokenOwnerRecord: PublicKey,
governanceAuthority: PublicKey,
proposalInstruction: PublicKey,
beneficiary: PublicKey
) => {
const args = new RemoveInstructionArgs()
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const keys = [
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwnerRecord,
isWritable: false,
isSigner: false,
},
{
pubkey: governanceAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: proposalInstruction,
isWritable: true,
isSigner: false,
},
{
pubkey: beneficiary,
isWritable: true,
isSigner: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,37 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { SetRealmAuthorityArgs } from './instructions'
export const withSetRealmAuthority = (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
realmAuthority: PublicKey,
newRealmAuthority: PublicKey
) => {
const args = new SetRealmAuthorityArgs({ newRealmAuthority })
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const keys = [
{
pubkey: realm,
isWritable: true,
isSigner: false,
},
{
pubkey: realmAuthority,
isWritable: false,
isSigner: true,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,51 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { SignOffProposalArgs } from './instructions'
export const withSignOffProposal = (
instructions: TransactionInstruction[],
programId: PublicKey,
proposal: PublicKey,
signatoryRecord: PublicKey,
signatory: PublicKey
) => {
const args = new SignOffProposalArgs()
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const keys = [
{
pubkey: proposal,
isWritable: true,
isSigner: false,
},
{
pubkey: signatoryRecord,
isWritable: true,
isSigner: false,
},
{
pubkey: signatory,
isWritable: false,
isSigner: true,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isSigner: false,
isWritable: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -0,0 +1,75 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js'
import { GOVERNANCE_SCHEMA } from './serialisation'
import { serialize } from 'borsh'
import { WithdrawGoverningTokensArgs } from './instructions'
import { GOVERNANCE_PROGRAM_SEED } from './accounts'
export const withWithdrawGoverningTokens = async (
instructions: TransactionInstruction[],
programId: PublicKey,
realm: PublicKey,
governingTokenDestination: PublicKey,
governingTokenMint: PublicKey,
governingTokenOwner: PublicKey,
tokenId: PublicKey
) => {
const args = new WithdrawGoverningTokensArgs()
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args))
const [tokenOwnerRecordAddress] = await PublicKey.findProgramAddress(
[
Buffer.from(GOVERNANCE_PROGRAM_SEED),
realm.toBuffer(),
governingTokenMint.toBuffer(),
governingTokenOwner.toBuffer(),
],
programId
)
const [governingTokenHoldingAddress] = await PublicKey.findProgramAddress(
[
Buffer.from(GOVERNANCE_PROGRAM_SEED),
realm.toBuffer(),
governingTokenMint.toBuffer(),
],
programId
)
const keys = [
{ pubkey: realm, isWritable: false, isSigner: false },
{
pubkey: governingTokenHoldingAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: governingTokenDestination,
isWritable: true,
isSigner: false,
},
{
pubkey: governingTokenOwner,
isWritable: false,
isSigner: true,
},
{
pubkey: tokenOwnerRecordAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenId,
isWritable: false,
isSigner: false,
},
]
instructions.push(
new TransactionInstruction({
keys,
programId,
data,
})
)
}

View File

@ -26,6 +26,8 @@
"@headlessui/react": "^1.0.0",
"@heroicons/react": "^1.0.1",
"@project-serum/anchor": "^0.10.0",
"@project-serum/borsh": "^0.2.2",
"@project-serum/common": "^0.0.1-beta.3",
"@project-serum/sol-wallet-adapter": "^0.2.0",
"@solana/spl-token": "^0.1.3",
"@solana/web3.js": "^1.5.0",

View File

@ -1,25 +0,0 @@
import ModalSection from '../components/ModalSection'
import PoolInfoCards from '../components/PoolInfoCards'
import HeroSection from '../components/HeroSection'
import ContentSectionAbout from '../components/ContentSectionAbout'
import ContentSectionSale from '../components/ContentSectionSale'
import ContentSectionRisks from '../components/ContentSectionRisks'
import FooterSection from '../components/FooterSection'
import ScrollToTop from '../components/ScrollToTop'
const ContributionPage = () => {
return (
<>
<HeroSection />
<PoolInfoCards />
<ContentSectionAbout />
<ContentSectionSale />
<ContentSectionRisks />
<ModalSection />
<FooterSection />
<ScrollToTop />
</>
)
}
export default ContributionPage

View File

@ -1,19 +0,0 @@
import ContentSectionAbout from '../components/ContentSectionAbout'
import ContentSectionSale from '../components/ContentSectionSale'
import ContentSectionRisks from '../components/ContentSectionRisks'
import FooterSection from '../components/FooterSection'
import HeroSectionLead from '../components/HeroSectionLead'
const LeadPage = () => {
return (
<>
<HeroSectionLead />
<ContentSectionAbout />
<ContentSectionSale />
<ContentSectionRisks />
<FooterSection />
</>
)
}
export default LeadPage

27
pages/ProposalPage.tsx Normal file
View File

@ -0,0 +1,27 @@
import useWalletStore from '../stores/useWalletStore'
const ProposalPage = () => {
const {
connected,
connection: { endpoint },
proposals: proposals,
} = useWalletStore((state) => state)
return (
<>
<p>connected:</p>
<pre>{connected}</pre>
<p>endpoint:</p>
<pre>{endpoint}</pre>
<p>proposals:</p>
{Object.entries(proposals || {}).map(([k, v]) => (
<>
<p>{k.toString()}</p>
<p>{JSON.stringify(v['info'])}</p>
</>
))}
</>
)
}
export default ProposalPage

View File

@ -1,15 +0,0 @@
import ContentSectionRedeem from '../components/ContentSectionRedeem'
import FooterSection from '../components/FooterSection'
import HeroSectionRedeem from '../components/HeroSectionRedeem'
const RedeemPage = () => {
return (
<>
<HeroSectionRedeem />
<ContentSectionRedeem />
<FooterSection />
</>
)
}
export default RedeemPage

View File

@ -1,22 +1,14 @@
import ContributionPage from './ContributionPage'
import LeadPage from './LeadPage'
import RedeemPage from './RedeemPage'
import ProposalPage from './ProposalPage'
import Notifications from '../components/Notification'
import NavBarBeta from '../components/NavBarBeta'
import usePool from '../hooks/usePool'
const Index = () => {
const { startIdo, endIdo } = usePool()
return (
<div className={`bg-bkg-1 text-white transition-all overflow-hidden`}>
<div className="w-screen h-2 bg-gradient-to-r from-mango-red via-mango-yellow to-mango-green"></div>
<NavBarBeta />
<Notifications />
{startIdo?.isAfter() && <LeadPage />}
{startIdo?.isBefore() && endIdo?.isAfter() && <ContributionPage />}
{endIdo?.isBefore() && <RedeemPage />}
<ProposalPage />
<div className="w-screen h-2 bg-gradient-to-r from-mango-red via-mango-yellow to-mango-green"></div>
</div>
)

View File

@ -1,54 +1,18 @@
import create, { State } from 'zustand'
import produce from 'immer'
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
import * as anchor from '@project-serum/anchor'
import { Connection, PublicKey } from '@solana/web3.js'
import { EndpointInfo, WalletAdapter } from '../@types/types'
// @ts-ignore
import poolIdl from '../idls/ido_pool'
import {
getOwnedTokenAccounts,
getMint,
ProgramAccount,
TokenAccount,
MintAccount,
getTokenAccount,
} from '../utils/tokens'
import { findLargestBalanceAccountForMint } from '../hooks/useLargestAccounts'
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { createAssociatedTokenAccount } from '../utils/associated'
import { sendTransaction } from '../utils/send'
import { ProgramAccount, TokenAccount, MintAccount } from '../utils/tokens'
import { getGovernanceAccounts } from '../models/api'
import { getAccountTypes, Proposal } from '../models/accounts'
import { DEFAULT_PROVIDER } from '../utils/wallet-adapters'
import { calculateNativeAmountUnsafe } from '../utils/balance'
export const ENDPOINTS: EndpointInfo[] = [
{
name: 'mainnet-beta',
url: 'https://mango.rpcpool.com',
websocket: 'https://mango.rpcpool.com',
programId: '6QXNNAPkPsWjd1j3qQJTvRFgSNPARMhF2tE8g1WeGyrM',
poolKey: 'AHBj9LAjxStT2YQHN6QdfHKpZLtEVr8ACqeFgYcPsTnr',
},
{
name: 'devnet',
url: 'https://api.devnet.solana.com',
websocket: 'https://api.devnet.solana.com',
programId: '2oBtRS2AAQfsMxXQfg41fKFY9zjvHwSSD7G5idrCFziV', // owned by devnet key
// programId: 'CRU6hX2GgtdabESgkoMswMrUdRFxHhCVYmS292VN1Nnn', // owned by governance
//poolKey: 'GvSyVjGwLBeWdURMLDmSffQPqA8g547A6TURbbBnDpa4', // governance test
// poolKey: '82ndgp58GXpwuLrEc9svHFdhiEsPaZoNUEWwgc79WHqk', // already over
poolKey: '5heMyYtJK1Us9Hx2w6s5rLDNj8RufeyCR1ZUJAVFLQL7', // long deposits
// poolKey: '7Dr2Ksnz5evoT9mEUgvvkmirH8KDC99b5oVPHbqSpx4K', // short deposit
//poolKey: 'CdKyD4Qazo72Bm6SsPBWrT1AnH1NEuoUzvQg7b67EBac', // not started yet
},
{
name: 'localnet',
url: 'http://localhost:8899',
websocket: 'http://localhost:8899',
programId: 'FF8zcQ1aEmyXeBt99hohoyYprgpEVmWsRK44qta3emno',
poolKey: '8gswb9g1JdYEVj662KXr9p6p9SMgR77NryyqvWn9GPXJ',
programId: 'GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J',
},
]
@ -57,20 +21,6 @@ const ENDPOINT = ENDPOINTS.find((e) => e.name === CLUSTER)
const DEFAULT_CONNECTION = new Connection(ENDPOINT.url, 'recent')
const WEBSOCKET_CONNECTION = new Connection(ENDPOINT.websocket, 'recent')
const PROGRAM_ID = new PublicKey(ENDPOINT.programId)
const POOL_PK = new PublicKey(ENDPOINT.poolKey)
interface PoolAccount {
distributionAuthority: PublicKey
endDepositsTs: anchor.BN
endIdoTs: anchor.BN
nonce: number
numIdoTokens: anchor.BN
poolUsdc: PublicKey
poolWatermelon: PublicKey
redeemableMint: PublicKey
startIdoTs: anchor.BN
watermelonMint: PublicKey
}
interface WalletStore extends State {
connected: boolean
@ -82,12 +32,8 @@ interface WalletStore extends State {
programId: PublicKey
}
current: WalletAdapter | undefined
proposals: undefined
providerUrl: string
provider: anchor.Provider | undefined
program: anchor.Program | undefined
pool: PoolAccount | undefined
mangoVault: TokenAccount | undefined
usdcVault: TokenAccount | undefined
tokenAccounts: ProgramAccount<TokenAccount>[]
mints: { [pubkey: string]: MintAccount }
set: (x: any) => void
@ -104,321 +50,31 @@ const useWalletStore = create<WalletStore>((set, get) => ({
programId: PROGRAM_ID,
},
current: null,
proposals: undefined,
providerUrl: DEFAULT_PROVIDER.url,
provider: undefined,
program: undefined,
pool: undefined,
mangoVault: undefined,
usdcVault: undefined,
tokenAccounts: [],
mints: {},
actions: {
async fetchPool() {
const connection = get().connection.current
const wallet = get().current
async fetchProposals() {
const endpoint = get().connection.endpoint
const programId = get().connection.programId
const set = get().set
// console.log('fetchPool', connection, poolIdl)
if (connection) {
const provider = new anchor.Provider(
connection,
wallet,
anchor.Provider.defaultOptions()
)
const program = new anchor.Program(poolIdl, programId, provider)
const pool = (await program.account.poolAccount.fetch(
POOL_PK
)) as PoolAccount
console.log('fetchProposals', endpoint)
const [usdcVault, mangoVault] = await Promise.all([
getTokenAccount(connection, pool.poolUsdc),
getTokenAccount(connection, pool.poolWatermelon),
])
// console.log('fetchPool', { program, pool, usdcVault, mangoVault })
set((state) => {
state.provider = provider
state.program = program
state.pool = pool
state.usdcVault = usdcVault.account
state.mangoVault = mangoVault.account
})
}
},
async fetchWalletTokenAccounts() {
const connection = get().connection.current
const connected = get().connected
const wallet = get().current
const walletOwner = wallet?.publicKey
const set = get().set
console.log(
'fetchWalletTokenAccounts',
connected,
walletOwner?.toString()
const proposals = await getGovernanceAccounts(
programId,
endpoint,
Proposal,
getAccountTypes(Proposal)
)
if (connected && walletOwner) {
const ownedTokenAccounts = await getOwnedTokenAccounts(
connection,
walletOwner
)
set((state) => {
state.tokenAccounts = ownedTokenAccounts
})
} else {
set((state) => {
state.tokenAccounts = []
})
}
},
async fetchUsdcVault() {
const connection = get().connection.current
const pool = get().pool
const set = get().set
if (!pool) return
const { account: vault } = await getTokenAccount(
connection,
pool.poolUsdc
)
// console.log('fetchUsdcVault', vault)
console.log('fetchProposals', proposals)
set((state) => {
state.usdcVault = vault
state.proposals = proposals
})
},
async fetchMints() {
const connection = get().connection.current
const pool = get().pool
const mangoVault = get().mangoVault
const usdcVault = get().usdcVault
const set = get().set
const mintKeys = [mangoVault.mint, usdcVault.mint, pool.redeemableMint]
const mints = await Promise.all(
mintKeys.map((pk) => getMint(connection, pk))
)
// console.log('fetchMints', mints)
set((state) => {
for (const pa of mints) {
state.mints[pa.publicKey.toBase58()] = pa.account
// console.log('mint', pa.publicKey.toBase58(), pa.account)
}
})
},
async fetchMNGOVault() {
const connection = get().connection.current
const pool = get().pool
const set = get().set
if (!pool) return
const { account: vault } = await getTokenAccount(
connection,
pool.poolWatermelon
)
// console.log('fetchMNGOVault', vault)
set((state) => {
state.mangoVault = vault
})
},
async fetchRedeemableMint() {
const connection = get().connection.current
const pool = get().pool
const set = get().set
const mintKeys = [pool.redeemableMint]
const mints = await Promise.all(
mintKeys.map((pk) => getMint(connection, pk))
)
// console.log('fetchMints', mints)
set((state) => {
for (const pa of mints) {
state.mints[pa.publicKey.toBase58()] = pa.account
// console.log('mint', pa.publicKey.toBase58(), pa.account)
}
})
},
async submitContribution(amount: number) {
console.log('submitContribution', amount)
const actions = get().actions
await actions.fetchWalletTokenAccounts()
const {
program,
provider,
pool,
tokenAccounts,
mints,
usdcVault,
current: wallet,
connection: { current: connection },
} = get()
const redeemable = findLargestBalanceAccountForMint(
mints,
tokenAccounts,
pool.redeemableMint
)
const usdc = findLargestBalanceAccountForMint(
mints,
tokenAccounts,
usdcVault.mint
)
const difference = amount - (redeemable?.balance || 0)
const [poolSigner] = await anchor.web3.PublicKey.findProgramAddress(
[pool.watermelonMint.toBuffer()],
program.programId
)
if (difference > 0) {
const depositAmount = calculateNativeAmountUnsafe(
mints,
usdcVault.mint,
difference
)
console.log(depositAmount.toString(), 'exchangeUsdcForReemable')
let redeemableAccPk = redeemable?.account?.publicKey
const transaction = new Transaction()
if (!redeemable) {
const [ins, pk] = await createAssociatedTokenAccount(
wallet.publicKey,
wallet.publicKey,
pool.redeemableMint
)
transaction.add(ins)
redeemableAccPk = pk
}
transaction.add(
program.instruction.exchangeUsdcForRedeemable(depositAmount, {
accounts: {
poolAccount: POOL_PK,
poolSigner: poolSigner,
redeemableMint: pool.redeemableMint,
poolUsdc: pool.poolUsdc,
userAuthority: provider.wallet.publicKey,
userUsdc: usdc.account.publicKey,
userRedeemable: redeemableAccPk,
tokenProgram: TOKEN_PROGRAM_ID,
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
},
})
)
await sendTransaction({ transaction, wallet, connection })
} else if (difference < 0) {
const withdrawAmount = calculateNativeAmountUnsafe(
mints,
usdcVault.mint,
-1 * difference
)
console.log(withdrawAmount.toString(), 'exchangeRedeemableForUsdc')
await program.rpc.exchangeRedeemableForUsdc(withdrawAmount, {
accounts: {
poolAccount: POOL_PK,
poolSigner: poolSigner,
redeemableMint: pool.redeemableMint,
poolUsdc: pool.poolUsdc,
userAuthority: provider.wallet.publicKey,
userUsdc: usdc.account.publicKey,
userRedeemable: redeemable.account.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
},
})
} else {
console.log('difference = 0 no submission needed', difference)
return
}
await actions.fetchWalletTokenAccounts()
actions.fetchUsdcVault()
},
async redeem() {
const actions = get().actions
await actions.fetchWalletTokenAccounts()
const {
program,
pool,
tokenAccounts,
mints,
current: wallet,
connection: { current: connection },
} = get()
const redeemable = findLargestBalanceAccountForMint(
mints,
tokenAccounts,
pool.redeemableMint
)
const watermelon = findLargestBalanceAccountForMint(
mints,
tokenAccounts,
pool.watermelonMint
)
console.log('exchangeRedeemableForMango', redeemable, watermelon)
const [poolSigner] = await anchor.web3.PublicKey.findProgramAddress(
[pool.watermelonMint.toBuffer()],
program.programId
)
const transaction = new Transaction()
let watermelonAccount = watermelon?.account?.publicKey
if (!watermelonAccount) {
const [ins, pk] = await createAssociatedTokenAccount(
wallet.publicKey,
wallet.publicKey,
pool.watermelonMint
)
transaction.add(ins)
watermelonAccount = pk
}
transaction.add(
program.instruction.exchangeRedeemableForWatermelon(
redeemable.account.account.amount,
{
accounts: {
poolAccount: POOL_PK,
poolSigner,
redeemableMint: pool.redeemableMint,
poolWatermelon: pool.poolWatermelon,
userAuthority: wallet.publicKey,
userWatermelon: watermelonAccount,
userRedeemable: redeemable.account.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
},
}
)
)
await sendTransaction({
transaction,
wallet,
connection,
sendingMessage: 'Sending redeem MNGO transaction...',
successMessage: 'MNGO redeemed successfully!',
})
await Promise.all([
actions.fetchPool(),
actions.fetchWalletTokenAccounts(),
])
},
},
set: (fn) => set(produce(fn)),
}))

View File

@ -0,0 +1,37 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js'
export async function createSetUpgradeAuthority(
programId: PublicKey,
upgradeAuthority: PublicKey,
newUpgradeAuthority: PublicKey,
bpfUpgradableLoaderId: PublicKey
) {
const [programDataAddress] = await PublicKey.findProgramAddress(
[programId.toBuffer()],
bpfUpgradableLoaderId
)
const keys = [
{
pubkey: programDataAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: upgradeAuthority,
isWritable: false,
isSigner: true,
},
{
pubkey: newUpgradeAuthority,
isWritable: false,
isSigner: false,
},
]
return new TransactionInstruction({
keys,
programId: bpfUpgradableLoaderId,
data: Buffer.from([4, 0, 0, 0]), // SetAuthority instruction bincode
})
}

View File

@ -0,0 +1,63 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js'
export async function createUpgradeInstruction(
programId: PublicKey,
bufferAddress: PublicKey,
upgradeAuthority: PublicKey,
spillAddress: PublicKey,
bpfUpgradableLoaderId: PublicKey
) {
const [programDataAddress] = await PublicKey.findProgramAddress(
[programId.toBuffer()],
bpfUpgradableLoaderId
)
const keys = [
{
pubkey: programDataAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: programId,
isWritable: true,
isSigner: false,
},
{
pubkey: bufferAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: spillAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isWritable: false,
isSigner: false,
},
{
pubkey: upgradeAuthority,
isWritable: false,
isSigner: true,
},
]
return new TransactionInstruction({
keys,
programId: bpfUpgradableLoaderId,
data: Buffer.from([3, 0, 0, 0]), // Upgrade instruction bincode
})
}

View File

@ -0,0 +1,57 @@
// Copied from Explorer code https://github.com/solana-labs/solana/blob/master/explorer/src/validators/accounts/token.ts
import { ParsedAccountData, AccountInfo, PublicKey } from '@solana/web3.js'
import {
Infer,
number,
optional,
enums,
boolean,
string,
type,
create,
} from 'superstruct'
import { PublicKeyFromString } from '../pubkey'
export type TokenAccountState = Infer<typeof AccountState>
const AccountState = enums(['initialized', 'uninitialized', 'frozen'])
const TokenAmount = type({
decimals: number(),
uiAmountString: string(),
amount: string(),
})
export type TokenAccountInfo = Infer<typeof TokenAccountInfo>
export const TokenAccountInfo = type({
mint: PublicKeyFromString,
owner: PublicKeyFromString,
tokenAmount: TokenAmount,
delegate: optional(PublicKeyFromString),
state: AccountState,
isNative: boolean(),
rentExemptReserve: optional(TokenAmount),
delegatedAmount: optional(TokenAmount),
closeAuthority: optional(PublicKeyFromString),
})
export function validateTokenAccount(
info: AccountInfo<Buffer | ParsedAccountData>,
mint: PublicKey | undefined
) {
if (!('parsed' in info.data && info.data.program === 'spl-token')) {
throw new Error('Invalid spl token account')
}
let tokenAccount: TokenAccountInfo
try {
tokenAccount = create(info.data.parsed.info, TokenAccountInfo)
} catch {
throw new Error('Invalid spl token account')
}
if (mint && tokenAccount.mint.toBase58() !== mint.toBase58()) {
throw new Error("Account mint doesn't match source account")
}
}

View File

@ -0,0 +1,68 @@
// Copied from Explorer code https://github.com/solana-labs/solana/blob/master/explorer/src/validators/accounts/upgradeable-program.ts
import { ParsedAccountData, AccountInfo, PublicKey } from '@solana/web3.js'
import { type, number, literal, nullable, Infer, create } from 'superstruct'
import { PublicKeyFromString } from '../pubkey'
export type ProgramAccountInfo = Infer<typeof ProgramAccountInfo>
export const ProgramAccountInfo = type({
programData: PublicKeyFromString,
})
export type ProgramAccount = Infer<typeof ProgramDataAccount>
export const ProgramAccount = type({
type: literal('program'),
info: ProgramAccountInfo,
})
export type ProgramDataAccountInfo = Infer<typeof ProgramDataAccountInfo>
export const ProgramDataAccountInfo = type({
authority: nullable(PublicKeyFromString),
// don't care about data yet
slot: number(),
})
export type ProgramDataAccount = Infer<typeof ProgramDataAccount>
export const ProgramDataAccount = type({
type: literal('programData'),
info: ProgramDataAccountInfo,
})
export type ProgramBufferAccountInfo = Infer<typeof ProgramBufferAccountInfo>
export const ProgramBufferAccountInfo = type({
authority: nullable(PublicKeyFromString),
// don't care about data yet
})
export type ProgramBufferAccount = Infer<typeof ProgramBufferAccount>
export const ProgramBufferAccount = type({
type: literal('buffer'),
info: ProgramBufferAccountInfo,
})
export function validateProgramBufferAccount(
info: AccountInfo<Buffer | ParsedAccountData>,
bufferAuthority: PublicKey
) {
if (
!('parsed' in info.data && info.data.program === 'bpf-upgradeable-loader')
) {
throw new Error('Invalid program buffer account')
}
let buffer: ProgramBufferAccount
try {
buffer = create(info.data.parsed, ProgramBufferAccount)
} catch {
throw new Error('Invalid program buffer account')
}
if (buffer.info.authority?.toBase58() !== bufferAuthority.toBase58()) {
throw new Error(
`Buffer authority must be set to governance account
${bufferAuthority.toBase58()}`
)
}
}

View File

@ -0,0 +1,8 @@
import { coerce, instance, string } from 'superstruct'
import { PublicKey } from '@solana/web3.js'
export const PublicKeyFromString = coerce(
instance(PublicKey),
string(),
(value) => new PublicKey(value)
)

97
utils/borsh.ts Normal file
View File

@ -0,0 +1,97 @@
import { PublicKey } from '@solana/web3.js'
import { BinaryReader, Schema, BorshError, BinaryWriter } from 'borsh'
;(BinaryReader.prototype as any).readPubkey = function () {
const reader = (this as unknown) as BinaryReader
const array = reader.readFixedArray(32)
return new PublicKey(array)
}
;(BinaryWriter.prototype as any).writePubkey = function (value: PublicKey) {
const writer = (this as unknown) as BinaryWriter
writer.writeFixedArray(value.toBuffer())
}
function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1)
}
function deserializeField(
schema: Schema,
fieldName: string,
fieldType: any,
reader: BinaryReader
): any {
try {
if (typeof fieldType === 'string') {
return (reader as any)[`read${capitalizeFirstLetter(fieldType)}`]()
}
if (fieldType instanceof Array) {
if (typeof fieldType[0] === 'number') {
return reader.readFixedArray(fieldType[0])
}
return reader.readArray(() =>
deserializeField(schema, fieldName, fieldType[0], reader)
)
}
if (fieldType.kind === 'option') {
const option = reader.readU8()
if (option) {
return deserializeField(schema, fieldName, fieldType.type, reader)
}
return undefined
}
return deserializeStruct(schema, fieldType, reader)
} catch (error) {
if (error instanceof BorshError) {
error.addToFieldPath(fieldName)
}
throw error
}
}
function deserializeStruct(
schema: Schema,
classType: any,
reader: BinaryReader
) {
const structSchema = schema.get(classType)
if (!structSchema) {
throw new BorshError(`Class ${classType.name} is missing in schema`)
}
if (structSchema.kind === 'struct') {
const result: any = {}
for (const [fieldName, fieldType] of schema.get(classType).fields) {
result[fieldName] = deserializeField(schema, fieldName, fieldType, reader)
}
return new classType(result)
}
if (structSchema.kind === 'enum') {
const idx = reader.readU8()
if (idx >= structSchema.values.length) {
throw new BorshError(`Enum index: ${idx} is out of range`)
}
const [fieldName, fieldType] = structSchema.values[idx]
const fieldValue = deserializeField(schema, fieldName, fieldType, reader)
return new classType({ [fieldName]: fieldValue })
}
throw new BorshError(
`Unexpected schema kind: ${structSchema.kind} for ${classType.constructor.name}`
)
}
/// Deserializes object from bytes using schema.
export function deserializeBorsh(
schema: Schema,
classType: any,
buffer: Buffer
): any {
const reader = new BinaryReader(buffer)
return deserializeStruct(schema, classType, reader)
}

View File

@ -724,6 +724,26 @@
snake-case "^3.0.4"
toml "^3.0.0"
"@project-serum/anchor@^0.11.1":
version "0.11.1"
resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.11.1.tgz#155bff2c70652eafdcfd5559c81a83bb19cec9ff"
integrity sha512-oIdm4vTJkUy6GmE6JgqDAuQPKI7XM4TPJkjtoIzp69RZe0iAD9JP2XHx7lV1jLdYXeYHqDXfBt3zcq7W91K6PA==
dependencies:
"@project-serum/borsh" "^0.2.2"
"@solana/web3.js" "^1.17.0"
base64-js "^1.5.1"
bn.js "^5.1.2"
bs58 "^4.0.1"
buffer-layout "^1.2.0"
camelcase "^5.3.1"
crypto-hash "^1.3.0"
eventemitter3 "^4.0.7"
find "^0.3.0"
js-sha256 "^0.9.0"
pako "^2.0.3"
snake-case "^3.0.4"
toml "^3.0.0"
"@project-serum/borsh@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@project-serum/borsh/-/borsh-0.2.2.tgz#63e558f2d6eb6ab79086bf499dea94da3182498f"
@ -732,6 +752,26 @@
bn.js "^5.1.2"
buffer-layout "^1.2.0"
"@project-serum/common@^0.0.1-beta.3":
version "0.0.1-beta.3"
resolved "https://registry.yarnpkg.com/@project-serum/common/-/common-0.0.1-beta.3.tgz#53586eaff9d9fd7e8938b1e12080c935b8b6ad07"
integrity sha512-gnQE/eUydTtto5okCgLWj1M97R9RRPJqnhKklikYI7jP/pnNhDmngSXC/dmfzED2GXSJEIKNIlxVw1k+E2Aw3w==
dependencies:
"@project-serum/serum" "^0.13.21"
bn.js "^5.1.2"
superstruct "0.8.3"
"@project-serum/serum@^0.13.21":
version "0.13.57"
resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.13.57.tgz#e194f5a7bb28c50cd3611d6f1559fd09e060b748"
integrity sha512-I638MKCEIQDv1WoPeRJhjOUmAn73fQCy1hNyb3f6GIecwxfiesoN6e0CClXyN7GG2nE7KPjFejGkF1KziJaltA==
dependencies:
"@project-serum/anchor" "^0.11.1"
"@solana/spl-token" "^0.1.6"
"@solana/web3.js" "^1.21.0"
bn.js "^5.1.2"
buffer-layout "^1.2.0"
"@project-serum/sol-wallet-adapter@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@project-serum/sol-wallet-adapter/-/sol-wallet-adapter-0.2.0.tgz#e1fa5508bf13110429bf26e10b818182015f2161"
@ -751,6 +791,13 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@solana/buffer-layout@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-3.0.0.tgz#b9353caeb9a1589cb77a1b145bcb1a9a93114326"
integrity sha512-MVdgAKKL39tEs0l8je0hKaXLQFb7Rdfb0Xg2LjFZd8Lfdazkg6xiS98uAZrEKvaoF3i4M95ei9RydkGIDMeo3w==
dependencies:
buffer "~6.0.3"
"@solana/spl-token@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.1.3.tgz#6bf7c1a74cd95dabe8b8164e4c13b987db5be3bd"
@ -762,6 +809,38 @@
buffer-layout "^1.2.0"
dotenv "8.2.0"
"@solana/spl-token@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.1.6.tgz#fa136b0a3db84f07a99bc0e54cf4e91f2d6da2e8"
integrity sha512-fYj+a3w1bqWN6Ibf85XF3h2JkuxevI3Spvqi+mjsNqVUEo2AgxxTZmujNLn/jIzQDNdWkBfF/wYzH5ikcGHmfw==
dependencies:
"@babel/runtime" "^7.10.5"
"@solana/web3.js" "^1.12.0"
bn.js "^5.1.0"
buffer "6.0.3"
buffer-layout "^1.2.0"
dotenv "10.0.0"
"@solana/web3.js@^1.12.0", "@solana/web3.js@^1.21.0":
version "1.24.0"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.24.0.tgz#4fe5194625d557c18effa3cc17579358c5bec75d"
integrity sha512-Br3r2YMoM6Ia7NlWVpe+w/cFlRMfW1yXCxy19rxjKZbxIb1i/iEGSOPGsEGCD6FgHJgyWGzD2tf4P1tWra5Fxg==
dependencies:
"@babel/runtime" "^7.12.5"
"@solana/buffer-layout" "^3.0.0"
bn.js "^5.0.0"
borsh "^0.4.0"
bs58 "^4.0.1"
buffer "6.0.1"
crypto-hash "^1.2.2"
jayson "^3.4.4"
js-sha3 "^0.8.0"
node-fetch "^2.6.1"
rpc-websockets "^7.4.2"
secp256k1 "^4.0.2"
superstruct "^0.14.2"
tweetnacl "^1.0.0"
"@solana/web3.js@^1.17.0":
version "1.20.0"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.20.0.tgz#9a1855a239c96c5b946bdbe4cc5e3768ee3b2a77"
@ -1581,7 +1660,7 @@ buffer@6.0.1:
base64-js "^1.3.1"
ieee754 "^1.2.1"
buffer@6.0.3:
buffer@6.0.3, buffer@~6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
dependencies:
@ -2224,6 +2303,11 @@ dot-case@^3.0.4:
no-case "^3.0.4"
tslib "^2.0.3"
dotenv@10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
dotenv@8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
@ -6022,6 +6106,14 @@ stylis@^4.0.3:
version "4.0.10"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
superstruct@0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.8.3.tgz#fb4d8901aca3bf9f79afab1bbab7a7f335cc4ef2"
integrity sha512-LbtbFpktW1FcwxVIJlxdk7bCyBq/GzOx2FSFLRLTUhWIA1gHkYPIl3aXRG5mBdGZtnPNT6t+4eEcLDCMOuBHww==
dependencies:
kind-of "^6.0.2"
tiny-invariant "^1.0.6"
superstruct@^0.14.2:
version "0.14.2"
resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b"
@ -6147,6 +6239,11 @@ timsort@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
tiny-invariant@^1.0.6:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tmpl@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"