add governance classes and fetch proposals
This commit is contained in:
parent
a18f4b38d4
commit
57bb583d64
|
@ -5,7 +5,6 @@ export interface EndpointInfo {
|
|||
url: string
|
||||
websocket: string
|
||||
programId: string
|
||||
poolKey: string
|
||||
}
|
||||
|
||||
export interface TokenAccount {
|
||||
|
|
|
@ -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
|
|
@ -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 "excess" 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
|
|
@ -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's order book.
|
||||
<br />
|
||||
<br />
|
||||
It will be up to us MNGO holders to provide that
|
||||
liquidity on day one. Let'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'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'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
|
|
@ -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'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
|
|
@ -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'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
|
|
@ -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're ready, deposit your USDC.</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!submitted &&
|
||||
!submitting &&
|
||||
!editContribution &&
|
||||
connected &&
|
||||
toLateToDeposit && (
|
||||
<>
|
||||
<h2>We'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've contributed ${usdFormat.format(contributionAmount)}.
|
||||
</h2>
|
||||
<p>Unlock to edit your contribution amount.</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{editContribution && !submitting && (
|
||||
<>
|
||||
<h2>
|
||||
You'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
|
|
@ -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 & 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
|
|
@ -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
|
|
@ -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'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's shape the future of
|
||||
Mango together.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 my-5 z-10">
|
||||
<RedeemModal />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeroSectionRedeem
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 }
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export enum GoverningTokenType {
|
||||
Community,
|
||||
Council,
|
||||
}
|
||||
|
||||
export enum GovernanceType {
|
||||
Account,
|
||||
Program,
|
||||
Mint,
|
||||
Token,
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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)),
|
||||
}))
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()}`
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
|
@ -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)
|
||||
}
|
99
yarn.lock
99
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue