New frontend only

This commit is contained in:
Jordan Prince 2021-06-02 09:35:28 -05:00
commit dfa519cc4f
846 changed files with 67285 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

13
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: cargo
directory: "/"
schedule:
interval: daily
time: "04:00"
timezone: America/Los_Angeles
labels:
- "automerge"
open-pull-requests-limit: 3
ignore:
- dependency-name: "cbindgen"

134
.github/workflows/pull-request.yml vendored Normal file
View File

@ -0,0 +1,134 @@
name: Pull Request
defaults:
run:
working-directory: ./rust
on:
pull_request:
push:
branches: [master]
jobs:
all_github_action_checks:
runs-on: ubuntu-latest
needs:
- rustfmt
- clippy
- cargo-build-test
steps:
- run: echo "Done"
working-directory: ./
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set env vars
run: |
source ci/rust-version.sh
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_STABLE }}
override: true
profile: minimal
components: rustfmt
- name: Run fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path ./rust/Cargo.toml --all -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set env vars
run: |
source ci/rust-version.sh
echo "RUST_NIGHTLY=$rust_nightly" >> $GITHUB_ENV
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_NIGHTLY }}
override: true
profile: minimal
components: clippy
- uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-clippy-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-clippy-
- name: Install dependencies
run: ./ci/install-build-deps.sh
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --manifest-path ./rust/Cargo.toml -Zunstable-options --workspace --all-targets -- --deny=warnings
cargo-build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set env vars
run: |
source ci/rust-version.sh
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
source ci/solana-version.sh
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_STABLE }}
override: true
profile: minimal
- uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
# target # Removed due to build dependency caching conflicts
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
- uses: actions/cache@v2
with:
path: |
~/.cargo/bin/rustfilt
key: cargo-bpf-bins-${{ runner.os }}
- uses: actions/cache@v2
with:
path: |
~/.cache
key: solana-${{ env.SOLANA_VERSION }}
- name: Install dependencies
run: |
./ci/install-build-deps.sh
./ci/install-program-deps.sh
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
- name: Build and test
run: ./ci/cargo-build-test.sh
- name: Upload programs
uses: actions/upload-artifact@v2
with:
name: programs
path: "rust/target/deploy/*.so"
if-no-files-found: error

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
node_modules/
build/
dist/
lib/
deploy/
docs/lockup-ui/
.DS_Store
*~
.idea
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.css
*.css.map
!js/packages/metaplex/src/fonts/fonts.css
!js/packages/metaplex/src/utils/globals.css
target
.env
.vscode
bin
config.json
node_modules
./package-lock.json
hfuzz_target
hfuzz_workspace
**/*.so
**/.DS_Store

39
.mergify.yml Normal file
View File

@ -0,0 +1,39 @@
# Validate your changes with:
#
# $ curl -F 'data=@.mergify.yml' https://gh.mergify.io/validate/
#
# https://doc.mergify.io/
pull_request_rules:
- name: automatic merge (squash) on CI success
conditions:
- check-success=all_github_action_checks
- label=automerge
- author≠@dont-squash-my-commits
actions:
merge:
method: squash
# Join the dont-squash-my-commits group if you won't like your commits squashed
- name: automatic merge (rebase) on CI success
conditions:
- check-success=all_github_action_checks
- label=automerge
- author=@dont-squash-my-commits
actions:
merge:
method: rebase
- name: remove automerge label on CI failure
conditions:
- label=automerge
- "#status-failure!=0"
actions:
label:
remove:
- automerge
comment:
message: automerge label removed due to a CI failure
- name: remove outdated reviews
conditions:
- base=master
actions:
dismiss_reviews:
changes_requested: true

0
.prettierrc Normal file
View File

20
.travis/affects.sh Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
#
# Check if files in the commit range match one or more prefixes
#
(
set -x
git diff --name-only "$TRAVIS_COMMIT_RANGE"
)
for file in $(git diff --name-only "$TRAVIS_COMMIT_RANGE"); do
for prefix in "$@"; do
if [[ $file =~ ^"$prefix" ]]; then
exit 0
fi
done
done
echo "No modifications to $*"
exit 1

202
LICENSE Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.

114
README.md Normal file
View File

@ -0,0 +1,114 @@
<p align="center">
<a href="https://metaplex.com">
<img alt="Metaplex" src="https://metaplex.com/meta.svg" width="250" />
</a>
</p>
Metaplex is a protocol built on top of Solana that allows:
- **Creating/Minting** non-fungible tokens;
- **Starting** a variety of auctions for primary/secondary sales;
- and **Visualizing** NFTs in a standard way across wallets and applications.
Metaplex is comprised of two core components: an on-chain program, and a self-hosted front-end web2 application.
## Installing
Clone the repo, and run `deploy-web.sh`.
```bash
$ git clone https://github.com/metaplex-foundation/metaplex.git
$ cd metaplex
$ cd js
$ ./deploy-web.sh
```
## Community
We have a few channels for contact:
- [Discord](https://discord.gg/metaplex)
- [@metaplex\_](https://twitter.com/metaplex_) on Twitter
- [GitHub Issues](https://github.com/metaplex-foundation/metaplex/issues)
# Protocol
## Non-fungible tokens
Metaplex's non-fungible-token standard is a part of the Solana Program Library (SPL), and can be characterized as a unique token with a fixed supply of 1 and 0 decimals. We extended the basic definition of an NFT on Solana to include additional metadata such as URI as defined in ERC-721 on Ethereum.
Below are the types of NFTs that can be created using the Metaplex protocol.
### **Master Edition**
A master edition token, when minted, represents both a non-fungible token on Solana and metadata that allows creators to control the provenance of prints created from the master edition.
Rights to create prints are tokenized itself, and the owner of the master edition can distribute tokens that allow users to create prints from master editions. Additionally, the creator can set the max supply of the master edition just like a regular mint on Solana, with the main difference being that each print is a numbered edition created from it.
A notable and desirable effect of master editions is that as prints are sold, the artwork will still remain visible in the artist's wallet as a master edition, while the prints appear in the purchaser's wallets.
### **Print**
A **print** represents a copy of an NFT, and is created from a Master Edition. Each print has an edition number associated with it.
Usually, prints are created as a part of an auction that has happened on Metaplex, but they could also be created by the creator manually.
For limited auctions, each print number is awarded based on the bid placement.
Prints can be created during [Open Edition](#open-edition) or [Limited Edition](#limited-edition) auction.
### Normal NFT
A normal NFT (like a Master Edition) when minted represents a non-fungible token on Solana and metadata, but lacks rights to print.
An example of a normal NFT would be an artwork that is a one-of-a-kind that, once sold, is no longer within the artist's own wallet, but is in the purchaser's wallet.
## Types of Auctions
Metaplex currently supports four types of auctions that are all derived from English auctions.
Basic parameters include:
- Auction start time
- Auction end time
- Reservation price
Additionally, Metaplex includes a novel concept of the participation NFT. Each bidding participant can be rewarded a unique NFT for participating in the auction.
The creator of an auction also has the ability to configure a minimal price that should be charged for redemption, with the option to set it as "free".
### Single Item
This type of auction can be used to sell normal NFTs and re-sell Prints, as well as the sale of Master Edition themselves (and the associated printing rights) if the artist so wishes. While this last behavior is not exposed in the current UI, it does exist in the protocol.
### Open Edition
An open edition auction requires the offering of a Master Edition NFT that specifically has no set supply. The auction will only create Prints of this item for bidders: each bidder is guaranteed to get a print, as there are no true "winners" of this auction type.
An open edition auction can either have a set fixed price (equivalent to a Buy Now sale), can be set to the bid price (Pay what you want), or can be free (Make any bid to get it for free).
### Limited Edition
For a limited edition auction, a Master Edition NFT (of limited or unlimited supply) may be provided to the auction with a number of copies as the set amount of winning places.
For each prize place, a Print will be minted in order of prize place, and awarded to the winning bidder of that place.
For example, the first place winner will win Print #1; the second place winner Print #2; and so on.
It is required for limited supply NFTs that there is at least as much supply remaining as there are desired winners in the auction.
### Tiered Auction
A tiered auction can contain a mix of the other three auction types as winning placements. For instance, the first place winner could win a Print of Limited Edition NFT A, while the second-place winner could win Normal NFT, and so on. Additionally, all participants who did not win any place could get a Participation NFT Print from a Master Edition (if the Master Edition had no supply limit).
## Royalties
Metaplex can seamlessly create on-chain artist splits that remove the awkwardness out of collaboration.
Tag each collaborator, set custom percentages, and youre off to the races. Each NFT can also be minted with configurable royalty payments that are then sent automatically back to the original creators whenever an artwork is resold on a Metaplex marketplace in the future.
## Storefronts
Metaplex's off-chain component allows creators to launch a custom storefront, similar to Shopify or WordPress. This open-source project provides a graphical interface to the on-chain Metaplex program, for creators, buyers, and curators of NFTs. The design and layout of storefronts can be customized to suit the needs of the entity creating it, either as a permanent storefront or an auction hub for a specific auction or collection.
All identification on the Storefront is based on wallet addresses. Creators and store admins sign through their wallets, and users place bids from connected wallets. Custom storefronts allow creators to create unique experiences per auction. Additionally, the Metaplex Foundation is working on multiple partnerships that will enable building immersive storefronts using VR/AR.

31
js/README.md Normal file
View File

@ -0,0 +1,31 @@
## Setup
Be sure to be running Node v12.16.2 and yarn version 1.22.10.
`yarn bootstrap`
Then run:
`yarn start`
You may have to rebuild your package more than one time to secure a
running environment.
## Known Issues
### Can't find CSS files in common
Common currently uses a less library to compile down less files into css in both the src directory for the TS server
in vscode to pick up and in the dist folder for importers like lending and proposal projects to pick up. If you do not see these files appear when running the `npm start lending` or other commands, and you see missing CSS errors,
you likely did not install the packages for common correctly. Try running:
`lerna exec npm install --scope @oyster/common` to specifically install packages for common.
Then, test that css transpiling is working:
`lerna exec npm watch-css-src --scope @oyster/common` and verify css files appear next to their less counterparts in src.
## ⚠️ Warning
Any content produced by Solana, or developer resources that Solana provides, are for educational and inspiration purposes only. Solana does not encourage, induce or sanction the deployment of any such applications in violation of applicable laws or regulations.

View File

@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35 35">
<defs>
<style>.prefix__cls-1{fill:#FFFFFF}</style>
</defs>
<g id="prefix__Group_26536" data-name="Group 26536" transform="translate(-80 -205)">
<path id="prefix__Shape" d="M23.588 0h-16v21.583h21.6v-16A5.585 5.585 0 0023.588 0z" class="prefix__cls-1"
transform="translate(85.739 205)"/>
<path id="prefix__Path_8749" d="M8.342 0H5.585A5.585 5.585 0 000 5.585v2.757h8.342z" class="prefix__cls-1"
data-name="Path 8749" transform="translate(80 205)"/>
<path id="prefix__Rectangle-path" d="M0 7.59h8.342v8.342H0z" class="prefix__cls-1"
transform="translate(80 210.739)"/>
<path id="prefix__Path_8750" d="M15.18 23.451h2.757a5.585 5.585 0 005.585-5.6V15.18H15.18z"
class="prefix__cls-1" data-name="Path 8750" transform="translate(91.478 216.478)"/>
<path id="prefix__Path_8751" d="M7.59 15.18h8.342v8.342H7.59z" class="prefix__cls-1" data-name="Path 8751"
transform="translate(85.739 216.478)"/>
<path id="prefix__Path_8752" d="M0 15.18v2.757a5.585 5.585 0 005.585 5.585h2.757V15.18z" class="prefix__cls-1"
data-name="Path 8752" transform="translate(80 216.478)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,31 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.4229 1.17953L16.7889 9.82025L18.9403 4.72235L28.4229 1.17953Z" fill="#E2761B" stroke="#E2761B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.56537 1.17953L13.1058 9.9021L11.0596 4.72235L1.56537 1.17953Z" fill="#E4761B" stroke="#E4761B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24.237 21.2087L21.1385 25.9558L27.7682 27.7798L29.674 21.3139L24.237 21.2087Z" fill="#E4761B" stroke="#E4761B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0.337646 21.3139L2.23182 27.7798L8.86144 25.9558L5.76294 21.2087L0.337646 21.3139Z" fill="#E4761B" stroke="#E4761B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.48724 13.1877L6.63983 15.9822L13.2227 16.2745L12.9888 9.20056L8.48724 13.1877Z" fill="#E4761B" stroke="#E4761B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5009 13.1877L16.9409 9.11871L16.7889 16.2745L23.36 15.9822L21.5009 13.1877Z" fill="#E4761B" stroke="#E4761B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.86145 25.9558L12.8135 24.0266L9.3993 21.3607L8.86145 25.9558Z" fill="#E4761B" stroke="#E4761B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.1748 24.0266L21.1385 25.9558L20.589 21.3607L17.1748 24.0266Z" fill="#E4761B" stroke="#E4761B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.1385 25.9558L17.1748 24.0266L17.4905 26.6106L17.4554 27.698L21.1385 25.9558Z" fill="#D7C1B3" stroke="#D7C1B3" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.86145 25.9558L12.5446 27.698L12.5212 26.6106L12.8135 24.0266L8.86145 25.9558Z" fill="#D7C1B3" stroke="#D7C1B3" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.603 19.6536L9.30573 18.6831L11.6325 17.6191L12.603 19.6536Z" fill="#233447" stroke="#233447" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.3852 19.6536L18.3557 17.6191L20.6942 18.6831L17.3852 19.6536Z" fill="#233447" stroke="#233447" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.86144 25.9558L9.42267 21.2087L5.76294 21.3139L8.86144 25.9558Z" fill="#CD6116" stroke="#CD6116" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.5773 21.2087L21.1385 25.9558L24.237 21.3139L20.5773 21.2087Z" fill="#CD6116" stroke="#CD6116" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23.36 15.9821L16.7889 16.2745L17.3969 19.6536L18.3674 17.6191L20.7058 18.6831L23.36 15.9821Z" fill="#CD6116" stroke="#CD6116" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.30498 18.6831L11.6435 17.6191L12.6022 19.6536L13.2219 16.2745L6.6391 15.9821L9.30498 18.6831Z" fill="#CD6116" stroke="#CD6116" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.63983 15.9821L9.39925 21.3607L9.30571 18.6831L6.63983 15.9821Z" fill="#E4751F" stroke="#E4751F" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.7059 18.6831L20.589 21.3607L23.3601 15.9821L20.7059 18.6831Z" fill="#E4751F" stroke="#E4751F" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.2227 16.2745L12.603 19.6536L13.3747 23.6407L13.5501 18.3908L13.2227 16.2745Z" fill="#E4751F" stroke="#E4751F" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.7889 16.2745L16.4732 18.3791L16.6135 23.6407L17.3969 19.6536L16.7889 16.2745Z" fill="#E4751F" stroke="#E4751F" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.3969 19.6536L16.6135 23.6407L17.1748 24.0266L20.589 21.3607L20.7059 18.6831L17.3969 19.6536Z" fill="#F6851B" stroke="#F6851B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.30573 18.6831L9.39927 21.3607L12.8135 24.0266L13.3747 23.6407L12.603 19.6536L9.30573 18.6831Z" fill="#F6851B" stroke="#F6851B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.4554 27.698L17.4905 26.6106L17.1982 26.3534H12.7901L12.5212 26.6106L12.5446 27.698L8.86145 25.9558L10.1476 27.0081L12.755 28.8205H17.2332L19.8523 27.0081L21.1385 25.9558L17.4554 27.698Z" fill="#C0AD9E" stroke="#C0AD9E" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.1747 24.0266L16.6135 23.6407H13.3747L12.8134 24.0266L12.5211 26.6106L12.79 26.3534H17.1981L17.4904 26.6106L17.1747 24.0266Z" fill="#161616" stroke="#161616" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M28.914 10.3815L29.9079 5.61097L28.4229 1.17953L17.1748 9.52794L21.501 13.1877L27.6162 14.9766L28.9725 13.3981L28.3879 12.9772L29.3233 12.1237L28.5983 11.5624L29.5337 10.8492L28.914 10.3815Z" fill="#763D16" stroke="#763D16" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0.0921021 5.61097L1.08596 10.3815L0.454568 10.8492L1.38996 11.5624L0.676724 12.1237L1.61212 12.9772L1.0275 13.3981L2.37213 14.9766L8.48728 13.1877L12.8135 9.52794L1.56535 1.17953L0.0921021 5.61097Z" fill="#763D16" stroke="#763D16" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27.6162 14.9766L21.501 13.1877L23.3601 15.9821L20.589 21.3607L24.237 21.3139H29.674L27.6162 14.9766Z" fill="#F6851B" stroke="#F6851B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.48728 13.1877L2.37213 14.9766L0.337646 21.3139H5.76294L9.39929 21.3607L6.63988 15.9821L8.48728 13.1877Z" fill="#F6851B" stroke="#F6851B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.7889 16.2745L17.1748 9.52795L18.952 4.72235H11.0596L12.8135 9.52795L13.2227 16.2745L13.363 18.4025L13.3747 23.6407H16.6135L16.6369 18.4025L16.7889 16.2745Z" fill="#F6851B" stroke="#F6851B" stroke-width="0.116924" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,11 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.8919 24.9666L29.9929 31.0468L23.6926 26.2361C22.228 25.1225 20.1169 26.069 19.9621 27.912L18.9563 40L17.3425 28.1849C17.0883 26.3085 14.85 25.4955 13.4628 26.7706L0 39.1648L11.8104 25.3842C13.0539 23.9365 12.142 21.6759 10.2463 21.5145L0.0773726 20.5234L10.5116 18.8753C12.3299 18.6247 13.1589 16.4477 11.9762 15.0334L6.8751 8.95323L13.1699 13.7639C14.6345 14.8775 16.7456 13.931 16.9004 12.088L17.9117 0L19.52 11.8151C19.7797 13.6915 22.018 14.5045 23.3997 13.2294L36.868 0.835189L25.0521 14.6158C23.8086 16.0635 24.7261 18.3241 26.6217 18.4855L36.7907 19.4766L26.3509 21.1247C24.5326 21.3753 23.7092 23.5523 24.8919 24.9666Z"
fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="11.8347" y1="14.2185" x2="21.4291" y2="22.4997"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FFC10B"></stop>
<stop offset="1" stop-color="#FB3F2E"></stop>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,13 @@
<svg width="530" height="530" xmlns="http://www.w3.org/2000/svg">
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="532" width="532" y="-1" x="-1"/>
</g>
<g>
<title>Layer 1</title>
<path id="svg_1" fill="#00FFA3" d="m88.88935,372.98201c3.193,-3.19 7.522,-4.982 12.035,-4.982l416.461,0c7.586,0 11.384,9.174 6.017,14.536l-82.291,82.226c-3.193,3.191 -7.522,4.983 -12.036,4.983l-416.4601,0c-7.5866,0 -11.3845,-9.174 -6.0178,-14.537l82.2919,-82.226z"/>
<path id="svg_2" fill="#00FFA3" d="m88.88935,65.9825c3.193,-3.1904 7.522,-4.9825 12.035,-4.9825l416.461,0c7.586,0 11.384,9.1739 6.017,14.5363l-82.291,82.2267c-3.193,3.19 -7.522,4.982 -12.036,4.982l-416.4601,0c-7.5866,0 -11.3845,-9.174 -6.0178,-14.536l82.2919,-82.2265z"/>
<path id="svg_3" fill="#00FFA3" d="m441.11135,219.1095c-3.193,-3.19 -7.522,-4.982 -12.036,-4.982l-416.4601,0c-7.5866,0 -11.3845,9.173 -6.0178,14.536l82.2919,82.226c3.193,3.19 7.522,4.983 12.035,4.983l416.461,0c7.586,0 11.384,-9.174 6.017,-14.537l-82.291,-82.226z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="226.000000pt" height="248.000000pt" viewBox="0 0 226.000000 248.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<g transform="translate(0.000000,248.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M482 2143 c-43 -21 -79 -64 -92 -111 -13 -48 -13 -246 0 -294 29
-103 92 -128 322 -128 l158 0 0 -588 0 -589 23 -43 c16 -30 37 -51 67 -67 39
-21 56 -23 182 -23 94 0 149 4 171 14 43 17 95 78 102 118 3 18 5 389 3 825
l-3 791 -31 39 c-59 74 -52 73 -484 73 -334 0 -389 -3 -418 -17z"/>
<path d="M1743 2144 c-57 -21 -126 -84 -154 -141 -35 -73 -32 -171 6 -241 54
-100 132 -146 245 -146 115 0 197 50 246 151 108 220 -110 459 -343 377z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 957 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.1 MiB

14
js/deploy-web.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
echo "Starting to deploy 'web', bootstrapping..."
yarn bootstrap
echo "Preparing 'common'..."
cd packages/common || exit
yarn prepare
yarn build-css
cd ../web || exit
echo "Prestarting 'web'..."
yarn prestart
echo "Building 'web'..."
# TODO: fix linting errors!
CI=false && yarn build
echo "#done"

22
js/lerna.json Normal file
View File

@ -0,0 +1,22 @@
{
"lerna": "3.4.3",
"version": "independent",
"packages": ["packages/*"],
"npmClient": "yarn",
"useWorkspaces": true,
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"ignoreChanges": [
"**/__tests__/**",
"**/*.md"
],
"command": {
"publish": {
"conventionalCommits": true,
"allowBranch": ["master", "next"],
"message": "chore(release): Publish"
}
}
}

77
js/package.json Normal file
View File

@ -0,0 +1,77 @@
{
"private": true,
"workspaces": {
"packages": [
"packages/*"
]
},
"keywords": [],
"license": "Apache-2.0",
"engines": {
"node": ">=6.0.0"
},
"scripts": {
"bootstrap": "lerna link && lerna bootstrap",
"build": "lerna run build",
"start": "CI=true lerna run start --scope @oyster/common --stream --parallel --scope web",
"lint": "eslint 'packages/*/{src,test}/**/*.ts' && prettier -c 'packages/*/{src,test}/**/*.ts'",
"lint:fix": "eslint --fix 'packages/*/{src,test}/**/*.ts' && prettier --write 'packages/*/{src,test}/**/*.ts'",
"deploy": "run-s deploy:docs build deploy:apps && gh-pages -d docs",
"deploy:docs": "lerna run docs",
"deploy:apps": "lerna run deploy:app",
"test": "lerna run test --concurrency 1 --stream"
},
"lint-staged": {
"packages/*/{src,test}/**/*.ts": [
"prettier --write"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"prettier": {
"arrowParens": "avoid",
"semi": true,
"singleQuote": true,
"trailingComma": "all"
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"dependencies": {
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"@commitlint/cli": "^8.2.0",
"@commitlint/config-conventional": "^8.2.0",
"@types/animejs": "^3.1.3",
"@types/jest": "^24.0.0",
"@types/react": "^16.9.50",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"eslint": "^6.6.0",
"eslint-config-prettier": "^6.15.0",
"gh-pages": "^3.1.0",
"husky": "^4.3.0",
"jest": "24.9.0",
"jest-config": "24.9.0",
"lerna": "3.22.1",
"lint-staged": "^10.5.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.2",
"rimraf": "^3.0.2",
"ts-jest": "^24.0.0",
"ts-node": "^9.0.0",
"typescript": "^4.1.3"
},
"resolutions": {
"react": "17.0.2",
"react-dom": "17.0.2"
}
}

View File

@ -0,0 +1,80 @@
{
"name": "@oyster/common",
"version": "0.0.1",
"description": "Oyster common utilities",
"main": "dist/lib/index.js",
"types": "dist/lib/index.d.ts",
"exports": {
".": "./dist/lib/"
},
"license": "Apache-2.0",
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=10"
},
"scripts": {
"build": "tsc",
"build-css": "less-watch-compiler src/ dist/lib/ --run-once",
"start": "npm-run-all --parallel watch watch-css watch-css-src",
"watch-css": "less-watch-compiler src/ dist/lib/",
"watch-css-src": "less-watch-compiler src/ src/",
"watch": "tsc --watch",
"test": "jest test",
"clean": "rm -rf dist",
"prepare": "run-s clean build"
},
"dependencies": {
"@project-serum/serum": "^0.13.34",
"@project-serum/sol-wallet-adapter": "^0.2.0",
"@solana/spl-token": "0.1.4",
"@solana/spl-token-swap": "0.1.0",
"@solana/wallet-base": "0.0.1",
"@solana/wallet-ledger": "0.0.1",
"@solana/web3.js": "^1.10.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"@types/chart.js": "^2.9.29",
"@types/echarts": "^4.9.0",
"@types/react-router-dom": "^5.1.6",
"@welldone-software/why-did-you-render": "^6.0.5",
"antd": "^4.6.6",
"bignumber.js": "^9.0.1",
"bn.js": "^5.1.3",
"borsh": "^0.4.0",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.0",
"eventemitter3": "^4.0.7",
"identicon.js": "^2.3.3",
"jazzicon": "^1.5.0",
"lodash": "^4.17.20",
"react": "17.0.2",
"react-dom": "17.0.2",
"typescript": "^4.1.3"
},
"devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/bs58": "^4.0.1",
"@types/identicon.js": "^2.3.0",
"@types/jest": "^24.9.1",
"@types/node": "^12.12.62",
"arweave-deploy": "^1.9.1",
"gh-pages": "^3.1.0",
"less": "^4.1.1",
"less-watch-compiler": "v1.14.6",
"prettier": "^2.1.2"
},
"files": [
"dist"
],
"peerDependencies": {
"react": "*",
"react-dom": "*"
},
"resolutions": {
"react": "17.0.2",
"react-dom": "17.0.2"
}
}

View File

@ -0,0 +1,333 @@
import { AccountLayout, MintLayout, Token } from '@solana/spl-token';
import {
Keypair,
PublicKey,
SystemProgram,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import {
programIds,
SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT,
} from '../utils/ids';
import { TokenAccount } from '../models/account';
import { cache, TokenAccountParser } from '../contexts/accounts';
export function ensureSplAccount(
instructions: TransactionInstruction[],
cleanupInstructions: TransactionInstruction[],
toCheck: TokenAccount,
payer: PublicKey,
amount: number,
signers: Keypair[],
) {
if (!toCheck.info.isNative) {
return toCheck.pubkey;
}
const account = createUninitializedAccount(
instructions,
payer,
amount,
signers,
);
instructions.push(
Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT,
account,
payer,
),
);
cleanupInstructions.push(
Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
account,
payer,
payer,
[],
),
);
return account;
}
export const DEFAULT_TEMP_MEM_SPACE = 65548;
export function createTempMemoryAccount(
instructions: TransactionInstruction[],
payer: PublicKey,
signers: Keypair[],
owner: PublicKey,
space = DEFAULT_TEMP_MEM_SPACE,
) {
const account = Keypair.generate();
instructions.push(
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: account.publicKey,
// 0 will evict/close account since it cannot pay rent
lamports: 0,
space: space,
programId: owner,
}),
);
signers.push(account);
return account.publicKey;
}
export function createUninitializedMint(
instructions: TransactionInstruction[],
payer: PublicKey,
amount: number,
signers: Keypair[],
) {
const account = Keypair.generate();
instructions.push(
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: account.publicKey,
lamports: amount,
space: MintLayout.span,
programId: TOKEN_PROGRAM_ID,
}),
);
signers.push(account);
return account.publicKey;
}
export function createUninitializedAccount(
instructions: TransactionInstruction[],
payer: PublicKey,
amount: number,
signers: Keypair[],
) {
const account = Keypair.generate();
instructions.push(
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: account.publicKey,
lamports: amount,
space: AccountLayout.span,
programId: TOKEN_PROGRAM_ID,
}),
);
signers.push(account);
return account.publicKey;
}
export function createAssociatedTokenAccountInstruction(
instructions: TransactionInstruction[],
associatedTokenAddress: PublicKey,
payer: PublicKey,
walletAddress: PublicKey,
splTokenMintAddress: PublicKey,
) {
const keys = [
{
pubkey: payer,
isSigner: true,
isWritable: true,
},
{
pubkey: associatedTokenAddress,
isSigner: false,
isWritable: true,
},
{
pubkey: walletAddress,
isSigner: false,
isWritable: false,
},
{
pubkey: splTokenMintAddress,
isSigner: false,
isWritable: false,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
{
pubkey: TOKEN_PROGRAM_ID,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
data: Buffer.from([]),
}),
);
}
export function createMint(
instructions: TransactionInstruction[],
payer: PublicKey,
mintRentExempt: number,
decimals: number,
owner: PublicKey,
freezeAuthority: PublicKey,
signers: Keypair[],
) {
const account = createUninitializedMint(
instructions,
payer,
mintRentExempt,
signers,
);
instructions.push(
Token.createInitMintInstruction(
TOKEN_PROGRAM_ID,
account,
decimals,
owner,
freezeAuthority,
),
);
return account;
}
export function createTokenAccount(
instructions: TransactionInstruction[],
payer: PublicKey,
accountRentExempt: number,
mint: PublicKey,
owner: PublicKey,
signers: Keypair[],
) {
const account = createUninitializedAccount(
instructions,
payer,
accountRentExempt,
signers,
);
instructions.push(
Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, mint, account, owner),
);
return account;
}
export function ensureWrappedAccount(
instructions: TransactionInstruction[],
cleanupInstructions: TransactionInstruction[],
toCheck: TokenAccount | undefined,
payer: PublicKey,
amount: number,
signers: Keypair[],
) {
if (toCheck && !toCheck.info.isNative) {
return toCheck.pubkey;
}
const TOKEN_PROGRAM_ID = programIds().token;
const account = Keypair.generate();
instructions.push(
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: account.publicKey,
lamports: amount,
space: AccountLayout.span,
programId: TOKEN_PROGRAM_ID,
}),
);
instructions.push(
Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT,
account.publicKey,
payer,
),
);
cleanupInstructions.push(
Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
account.publicKey,
payer,
payer,
[],
),
);
signers.push(account);
return account.publicKey;
}
// TODO: check if one of to accounts needs to be native sol ... if yes unwrap it ...
export function findOrCreateAccountByMint(
payer: PublicKey,
owner: PublicKey,
instructions: TransactionInstruction[],
cleanupInstructions: TransactionInstruction[],
accountRentExempt: number,
mint: PublicKey, // use to identify same type
signers: Keypair[],
excluded?: Set<string>,
): PublicKey {
const accountToFind = mint.toBase58();
const account = cache
.byParser(TokenAccountParser)
.map(id => cache.get(id))
.find(
acc =>
acc !== undefined &&
acc.info.mint.toBase58() === accountToFind &&
acc.info.owner.toBase58() === owner.toBase58() &&
(excluded === undefined || !excluded.has(acc.pubkey.toBase58())),
);
const isWrappedSol = accountToFind === WRAPPED_SOL_MINT.toBase58();
let toAccount: PublicKey;
if (account && !isWrappedSol) {
toAccount = account.pubkey;
} else {
// creating depositor pool account
toAccount = createTokenAccount(
instructions,
payer,
accountRentExempt,
mint,
owner,
signers,
);
if (isWrappedSol) {
cleanupInstructions.push(
Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
toAccount,
payer,
payer,
[],
),
);
}
}
return toAccount;
}

View File

@ -0,0 +1,954 @@
import {
AccountInfo,
PublicKey,
SystemProgram,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { programIds } from '../utils/ids';
import { deserializeUnchecked, serialize } from 'borsh';
import BN from 'bn.js';
import { AccountParser } from '../contexts';
import moment from 'moment';
export const AUCTION_PREFIX = 'auction';
export const METADATA = 'metadata';
export const EXTENDED = 'extended';
export enum AuctionState {
Created = 0,
Started,
Ended,
}
export enum BidStateType {
EnglishAuction = 0,
OpenEdition = 1,
}
export class Bid {
key: PublicKey;
amount: BN;
constructor(args: { key: PublicKey; amount: BN }) {
this.key = args.key;
this.amount = args.amount;
}
}
export class BidState {
type: BidStateType;
bids: Bid[];
max: BN;
public getWinnerIndex(bidder: PublicKey): number | null {
if (!this.bids) return null;
const index = this.bids.findIndex(
b => b.key.toBase58() === bidder.toBase58(),
);
// auction stores data in reverse order
if (index !== -1) {
const zeroBased = this.bids.length - index - 1;
return zeroBased < this.max.toNumber() ? zeroBased : null;
} else return null;
}
constructor(args: { type: BidStateType; bids: Bid[]; max: BN }) {
this.type = args.type;
this.bids = args.bids;
this.max = args.max;
}
}
export const AuctionParser: AccountParser = (
pubkey: PublicKey,
account: AccountInfo<Buffer>,
) => ({
pubkey,
account,
info: decodeAuction(account.data),
});
export const decodeAuction = (buffer: Buffer) => {
return deserializeUnchecked(
AUCTION_SCHEMA,
AuctionData,
buffer,
) as AuctionData;
};
export const BidderPotParser: AccountParser = (
pubkey: PublicKey,
account: AccountInfo<Buffer>,
) => ({
pubkey,
account,
info: decodeBidderPot(account.data),
});
export const decodeBidderPot = (buffer: Buffer) => {
return deserializeUnchecked(AUCTION_SCHEMA, BidderPot, buffer) as BidderPot;
};
export const BidderMetadataParser: AccountParser = (
pubkey: PublicKey,
account: AccountInfo<Buffer>,
) => ({
pubkey,
account,
info: decodeBidderMetadata(account.data),
});
export const decodeBidderMetadata = (buffer: Buffer) => {
return deserializeUnchecked(
AUCTION_SCHEMA,
BidderMetadata,
buffer,
) as BidderMetadata;
};
export const BASE_AUCTION_DATA_SIZE =
32 + 32 + 32 + 9 + 9 + 9 + 9 + 1 + 32 + 1 + 8 + 8;
export enum PriceFloorType {
None = 0,
Minimum = 1,
BlindedPrice = 2,
}
export class PriceFloor {
type: PriceFloorType;
// It's an array of 32 u8s, when minimum, only first 4 are used (a u64), when blinded price, the entire
// thing is a hash and not actually a public key, and none is all zeroes
hash: PublicKey;
constructor(args: { type: PriceFloorType; hash: PublicKey }) {
this.type = args.type;
this.hash = args.hash;
}
}
export class AuctionDataExtended {
/// Total uncancelled bids
totalUncancelledBids: BN;
tickSize: BN | null;
gapTickSizePercentage: number | null;
constructor(args: {
totalUncancelledBids: BN;
tickSize: BN | null;
gapTickSizePercentage: number | null;
}) {
this.totalUncancelledBids = args.totalUncancelledBids;
this.tickSize = args.tickSize;
this.gapTickSizePercentage = args.gapTickSizePercentage;
}
}
export interface CountdownState {
days: number;
hours: number;
minutes: number;
seconds: number;
}
export class AuctionData {
/// Pubkey of the authority with permission to modify this auction.
authority: PublicKey;
/// Token mint for the SPL token being used to bid
tokenMint: PublicKey;
/// The time the last bid was placed, used to keep track of auction timing.
lastBid: BN | null;
/// Slot time the auction was officially ended by.
endedAt: BN | null;
/// End time is the cut-off point that the auction is forced to end by.
endAuctionAt: BN | null;
/// Gap time is the amount of time in slots after the previous bid at which the auction ends.
auctionGap: BN | null;
/// Minimum price for any bid to meet.
priceFloor: PriceFloor;
/// The state the auction is in, whether it has started or ended.
state: AuctionState;
/// Auction Bids, each user may have one bid open at a time.
bidState: BidState;
/// Total uncancelled bids
totalUncancelledBids: BN;
/// Used for precalculation on the front end, not a backend key
bidRedemptionKey?: PublicKey;
public timeToEnd(): CountdownState {
const now = moment().unix();
const ended = { days: 0, hours: 0, minutes: 0, seconds: 0 };
let endAt = this.endedAt?.toNumber() || 0;
if (this.auctionGap && this.lastBid) {
endAt = Math.max(
endAt,
this.auctionGap.toNumber() + this.lastBid.toNumber(),
);
}
let delta = endAt - now;
if (!endAt || delta <= 0) return ended;
const days = Math.floor(delta / 86400);
delta -= days * 86400;
const hours = Math.floor(delta / 3600) % 24;
delta -= hours * 3600;
const minutes = Math.floor(delta / 60) % 60;
delta -= minutes * 60;
const seconds = Math.floor(delta % 60);
return { days, hours, minutes, seconds };
}
public ended() {
const now = moment().unix();
if (!this.endedAt) return false;
if (this.endedAt.toNumber() > now) return false;
if (this.endedAt.toNumber() < now) {
if (this.auctionGap && this.lastBid) {
const newEnding = this.auctionGap.toNumber() + this.lastBid.toNumber();
return newEnding < now;
} else return true;
}
}
constructor(args: {
authority: PublicKey;
tokenMint: PublicKey;
lastBid: BN | null;
endedAt: BN | null;
endAuctionAt: BN | null;
auctionGap: BN | null;
priceFloor: PriceFloor;
state: AuctionState;
bidState: BidState;
totalUncancelledBids: BN;
}) {
this.authority = args.authority;
this.tokenMint = args.tokenMint;
this.lastBid = args.lastBid;
this.endedAt = args.endedAt;
this.endAuctionAt = args.endAuctionAt;
this.auctionGap = args.auctionGap;
this.priceFloor = args.priceFloor;
this.state = args.state;
this.bidState = args.bidState;
this.totalUncancelledBids = args.totalUncancelledBids;
}
}
export const BIDDER_METADATA_LEN = 32 + 32 + 8 + 8 + 1;
export class BidderMetadata {
// Relationship with the bidder who's metadata this covers.
bidderPubkey: PublicKey;
// Relationship with the auction this bid was placed on.
auctionPubkey: PublicKey;
// Amount that the user bid.
lastBid: BN;
// Tracks the last time this user bid.
lastBidTimestamp: BN;
// Whether the last bid the user made was cancelled. This should also be enough to know if the
// user is a winner, as if cancelled it implies previous bids were also cancelled.
cancelled: boolean;
constructor(args: {
bidderPubkey: PublicKey;
auctionPubkey: PublicKey;
lastBid: BN;
lastBidTimestamp: BN;
cancelled: boolean;
}) {
this.bidderPubkey = args.bidderPubkey;
this.auctionPubkey = args.auctionPubkey;
this.lastBid = args.lastBid;
this.lastBidTimestamp = args.lastBidTimestamp;
this.cancelled = args.cancelled;
}
}
export const BIDDER_POT_LEN = 32 + 32 + 32 + 1;
export class BidderPot {
/// Points at actual pot that is a token account
bidderPot: PublicKey;
bidderAct: PublicKey;
auctionAct: PublicKey;
emptied: boolean;
constructor(args: {
bidderPot: PublicKey;
bidderAct: PublicKey;
auctionAct: PublicKey;
emptied: boolean;
}) {
this.bidderPot = args.bidderPot;
this.bidderAct = args.bidderAct;
this.auctionAct = args.auctionAct;
this.emptied = args.emptied;
}
}
export enum WinnerLimitType {
Unlimited = 0,
Capped = 1,
}
export class WinnerLimit {
type: WinnerLimitType;
usize: BN;
constructor(args: { type: WinnerLimitType; usize: BN }) {
this.type = args.type;
this.usize = args.usize;
}
}
class CreateAuctionArgs {
instruction: number = 1;
/// How many winners are allowed for this auction. See AuctionData.
winners: WinnerLimit;
/// End time is the cut-off point that the auction is forced to end by. See AuctionData.
endAuctionAt: BN | null;
/// Gap time is how much time after the previous bid where the auction ends. See AuctionData.
auctionGap: BN | null;
/// Token mint for the SPL token used for bidding.
tokenMint: PublicKey;
/// Authority
authority: PublicKey;
/// The resource being auctioned. See AuctionData.
resource: PublicKey;
priceFloor: PriceFloor;
constructor(args: {
winners: WinnerLimit;
endAuctionAt: BN | null;
auctionGap: BN | null;
tokenMint: PublicKey;
authority: PublicKey;
resource: PublicKey;
priceFloor: PriceFloor;
}) {
this.winners = args.winners;
this.endAuctionAt = args.endAuctionAt;
this.auctionGap = args.auctionGap;
this.tokenMint = args.tokenMint;
this.authority = args.authority;
this.resource = args.resource;
this.priceFloor = args.priceFloor;
}
}
class StartAuctionArgs {
instruction: number = 4;
resource: PublicKey;
constructor(args: { resource: PublicKey }) {
this.resource = args.resource;
}
}
class PlaceBidArgs {
instruction: number = 6;
resource: PublicKey;
amount: BN;
constructor(args: { resource: PublicKey; amount: BN }) {
this.resource = args.resource;
this.amount = args.amount;
}
}
class CancelBidArgs {
instruction: number = 0;
resource: PublicKey;
constructor(args: { resource: PublicKey }) {
this.resource = args.resource;
}
}
export const AUCTION_SCHEMA = new Map<any, any>([
[
CreateAuctionArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['winners', WinnerLimit],
['endAuctionAt', { kind: 'option', type: 'u64' }],
['auctionGap', { kind: 'option', type: 'u64' }],
['tokenMint', 'pubkey'],
['authority', 'pubkey'],
['resource', 'pubkey'],
['priceFloor', PriceFloor],
],
},
],
[
WinnerLimit,
{
kind: 'struct',
fields: [
['type', 'u8'],
['usize', 'u64'],
],
},
],
[
StartAuctionArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['resource', 'pubkey'],
],
},
],
[
PlaceBidArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['amount', 'u64'],
['resource', 'pubkey'],
],
},
],
[
CancelBidArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['resource', 'pubkey'],
],
},
],
[
AuctionData,
{
kind: 'struct',
fields: [
['authority', 'pubkey'],
['tokenMint', 'pubkey'],
['lastBid', { kind: 'option', type: 'u64' }],
['endedAt', { kind: 'option', type: 'u64' }],
['endAuctionAt', { kind: 'option', type: 'u64' }],
['auctionGap', { kind: 'option', type: 'u64' }],
['priceFloor', PriceFloor],
['state', 'u8'],
['bidState', BidState],
],
},
],
[
AuctionDataExtended,
{
kind: 'struct',
fields: [
['totalUncancelledBids', 'u64'],
['tickSize', { kind: 'option', type: 'u64' }],
['gapTickSizePercentage', { kind: 'option', type: 'u8' }],
],
},
],
[
PriceFloor,
{
kind: 'struct',
fields: [
['type', 'u8'],
['hash', 'pubkey'],
],
},
],
[
BidState,
{
kind: 'struct',
fields: [
['type', 'u8'],
['bids', [Bid]],
['max', 'u64'],
],
},
],
[
Bid,
{
kind: 'struct',
fields: [
['key', 'pubkey'],
['amount', 'u64'],
],
},
],
[
BidderMetadata,
{
kind: 'struct',
fields: [
['bidderPubkey', 'pubkey'],
['auctionPubkey', 'pubkey'],
['lastBid', 'u64'],
['lastBidTimestamp', 'u64'],
['cancelled', 'u8'],
],
},
],
[
BidderPot,
{
kind: 'struct',
fields: [
['bidderPot', 'pubkey'],
['bidderAct', 'pubkey'],
['auctionAct', 'pubkey'],
['emptied', 'u8'],
],
},
],
]);
export const decodeAuctionData = (buffer: Buffer) => {
return deserializeUnchecked(
AUCTION_SCHEMA,
AuctionData,
buffer,
) as AuctionData;
};
export async function createAuction(
winners: WinnerLimit,
resource: PublicKey,
endAuctionAt: BN | null,
auctionGap: BN | null,
tokenMint: PublicKey,
authority: PublicKey,
creator: PublicKey,
instructions: TransactionInstruction[],
) {
const auctionProgramId = programIds().auction;
const data = Buffer.from(
serialize(
AUCTION_SCHEMA,
new CreateAuctionArgs({
winners,
resource,
endAuctionAt,
auctionGap,
tokenMint,
authority,
priceFloor: new PriceFloor({
type: PriceFloorType.None,
hash: SystemProgram.programId,
}),
}),
),
);
const auctionKey: PublicKey = (
await PublicKey.findProgramAddress(
[
Buffer.from(AUCTION_PREFIX),
auctionProgramId.toBuffer(),
resource.toBuffer(),
],
auctionProgramId,
)
)[0];
const keys = [
{
pubkey: creator,
isSigner: true,
isWritable: true,
},
{
pubkey: auctionKey,
isSigner: false,
isWritable: true,
},
{
pubkey: await getAuctionExtended({ auctionProgramId, resource }),
isSigner: false,
isWritable: true,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: auctionProgramId,
data: data,
}),
);
}
export async function startAuction(
resource: PublicKey,
creator: PublicKey,
instructions: TransactionInstruction[],
) {
const auctionProgramId = programIds().auction;
const data = Buffer.from(
serialize(
AUCTION_SCHEMA,
new StartAuctionArgs({
resource,
}),
),
);
const auctionKey: PublicKey = (
await PublicKey.findProgramAddress(
[
Buffer.from(AUCTION_PREFIX),
auctionProgramId.toBuffer(),
resource.toBuffer(),
],
auctionProgramId,
)
)[0];
const keys = [
{
pubkey: creator,
isSigner: false,
isWritable: true,
},
{
pubkey: auctionKey,
isSigner: false,
isWritable: true,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: auctionProgramId,
data: data,
}),
);
}
export async function placeBid(
bidderPubkey: PublicKey,
bidderTokenPubkey: PublicKey,
bidderPotTokenPubkey: PublicKey,
tokenMintPubkey: PublicKey,
transferAuthority: PublicKey,
payer: PublicKey,
resource: PublicKey,
amount: BN,
instructions: TransactionInstruction[],
) {
const auctionProgramId = programIds().auction;
const data = Buffer.from(
serialize(
AUCTION_SCHEMA,
new PlaceBidArgs({
resource,
amount,
}),
),
);
const auctionKey: PublicKey = (
await PublicKey.findProgramAddress(
[
Buffer.from(AUCTION_PREFIX),
auctionProgramId.toBuffer(),
resource.toBuffer(),
],
auctionProgramId,
)
)[0];
const bidderPotKey = await getBidderPotKey({
auctionProgramId,
auctionKey,
bidderPubkey,
});
const bidderMetaKey: PublicKey = (
await PublicKey.findProgramAddress(
[
Buffer.from(AUCTION_PREFIX),
auctionProgramId.toBuffer(),
auctionKey.toBuffer(),
bidderPubkey.toBuffer(),
Buffer.from('metadata'),
],
auctionProgramId,
)
)[0];
const keys = [
{
pubkey: bidderPubkey,
isSigner: true,
isWritable: false,
},
{
pubkey: bidderTokenPubkey,
isSigner: false,
isWritable: true,
},
{
pubkey: bidderPotKey,
isSigner: false,
isWritable: true,
},
{
pubkey: bidderPotTokenPubkey,
isSigner: false,
isWritable: true,
},
{
pubkey: bidderMetaKey,
isSigner: false,
isWritable: true,
},
{
pubkey: auctionKey,
isSigner: false,
isWritable: true,
},
{
pubkey: await getAuctionExtended({ auctionProgramId, resource }),
isSigner: false,
isWritable: true,
},
{
pubkey: tokenMintPubkey,
isSigner: false,
isWritable: true,
},
{
pubkey: transferAuthority,
isSigner: true,
isWritable: false,
},
{
pubkey: payer,
isSigner: true,
isWritable: false,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
{
pubkey: programIds().token,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: auctionProgramId,
data: data,
}),
);
return {
amount,
};
}
export async function getBidderPotKey({
auctionProgramId,
auctionKey,
bidderPubkey,
}: {
auctionProgramId: PublicKey;
auctionKey: PublicKey;
bidderPubkey: PublicKey;
}): Promise<PublicKey> {
return (
await PublicKey.findProgramAddress(
[
Buffer.from(AUCTION_PREFIX),
auctionProgramId.toBuffer(),
auctionKey.toBuffer(),
bidderPubkey.toBuffer(),
],
auctionProgramId,
)
)[0];
}
export async function getAuctionExtended({
auctionProgramId,
resource,
}: {
auctionProgramId: PublicKey;
resource: PublicKey;
}): Promise<PublicKey> {
return (
await PublicKey.findProgramAddress(
[
Buffer.from(AUCTION_PREFIX),
auctionProgramId.toBuffer(),
resource.toBuffer(),
Buffer.from(EXTENDED),
],
auctionProgramId,
)
)[0];
}
export async function cancelBid(
bidderPubkey: PublicKey,
bidderTokenPubkey: PublicKey,
bidderPotTokenPubkey: PublicKey,
tokenMintPubkey: PublicKey,
resource: PublicKey,
instructions: TransactionInstruction[],
) {
const auctionProgramId = programIds().auction;
const data = Buffer.from(
serialize(
AUCTION_SCHEMA,
new CancelBidArgs({
resource,
}),
),
);
const auctionKey: PublicKey = (
await PublicKey.findProgramAddress(
[
Buffer.from(AUCTION_PREFIX),
auctionProgramId.toBuffer(),
resource.toBuffer(),
],
auctionProgramId,
)
)[0];
const bidderPotKey = await getBidderPotKey({
auctionProgramId,
auctionKey,
bidderPubkey,
});
const bidderMetaKey: PublicKey = (
await PublicKey.findProgramAddress(
[
Buffer.from(AUCTION_PREFIX),
auctionProgramId.toBuffer(),
auctionKey.toBuffer(),
bidderPubkey.toBuffer(),
Buffer.from('metadata'),
],
auctionProgramId,
)
)[0];
const keys = [
{
pubkey: bidderPubkey,
isSigner: true,
isWritable: false,
},
{
pubkey: bidderTokenPubkey,
isSigner: false,
isWritable: true,
},
{
pubkey: bidderPotKey,
isSigner: false,
isWritable: true,
},
{
pubkey: bidderPotTokenPubkey,
isSigner: false,
isWritable: true,
},
{
pubkey: bidderMetaKey,
isSigner: false,
isWritable: true,
},
{
pubkey: auctionKey,
isSigner: false,
isWritable: true,
},
{
pubkey: await getAuctionExtended({ auctionProgramId, resource }),
isSigner: false,
isWritable: true,
},
{
pubkey: tokenMintPubkey,
isSigner: false,
isWritable: true,
},
{
pubkey: SYSVAR_CLOCK_PUBKEY,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
{
pubkey: programIds().token,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: auctionProgramId,
data: data,
}),
);
}

View File

@ -0,0 +1,4 @@
export * from './account';
export * from './metadata';
export * from './vault';
export * from './auction';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,707 @@
import {
PublicKey,
SystemProgram,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { programIds } from '../utils/ids';
import { deserializeUnchecked, serialize } from 'borsh';
import BN from 'bn.js';
export const VAULT_PREFIX = 'vault';
export enum VaultKey {
VaultV1 = 0,
SafetyDepositBoxV1 = 1,
ExternalPriceAccountV1 = 2,
}
export enum VaultState {
Inactive = 0,
Active = 1,
Combined = 2,
Deactivated = 3,
}
export const MAX_VAULT_SIZE =
1 + 32 + 32 + 32 + 32 + 1 + 32 + 1 + 32 + 1 + 1 + 8;
export const MAX_EXTERNAL_ACCOUNT_SIZE = 1 + 8 + 32 + 1;
export class Vault {
key: VaultKey;
/// Store token program used
tokenProgram: PublicKey;
/// Mint that produces the fractional shares
fractionMint: PublicKey;
/// Authority who can make changes to the vault
authority: PublicKey;
/// treasury where fractional shares are held for redemption by authority
fractionTreasury: PublicKey;
/// treasury where monies are held for fractional share holders to redeem(burn) shares once buyout is made
redeemTreasury: PublicKey;
/// Can authority mint more shares from fraction_mint after activation
allowFurtherShareCreation: boolean;
/// Must point at an ExternalPriceAccount, which gives permission and price for buyout.
pricingLookupAddress: PublicKey;
/// In inactive state, we use this to set the order key on Safety Deposit Boxes being added and
/// then we increment it and save so the next safety deposit box gets the next number.
/// In the Combined state during token redemption by authority, we use it as a decrementing counter each time
/// The authority of the vault withdrawals a Safety Deposit contents to count down how many
/// are left to be opened and closed down. Once this hits zero, and the fraction mint has zero shares,
/// then we can deactivate the vault.
tokenTypeCount: number;
state: VaultState;
/// Once combination happens, we copy price per share to vault so that if something nefarious happens
/// to external price account, like price change, we still have the math 'saved' for use in our calcs
lockedPricePerShare: BN;
constructor(args: {
tokenProgram: PublicKey;
fractionMint: PublicKey;
authority: PublicKey;
fractionTreasury: PublicKey;
redeemTreasury: PublicKey;
allowFurtherShareCreation: boolean;
pricingLookupAddress: PublicKey;
tokenTypeCount: number;
state: VaultState;
lockedPricePerShare: BN;
}) {
this.key = VaultKey.VaultV1;
this.tokenProgram = args.tokenProgram;
this.fractionMint = args.fractionMint;
this.authority = args.authority;
this.fractionTreasury = args.fractionTreasury;
this.redeemTreasury = args.redeemTreasury;
this.allowFurtherShareCreation = args.allowFurtherShareCreation;
this.pricingLookupAddress = args.pricingLookupAddress;
this.tokenTypeCount = args.tokenTypeCount;
this.state = args.state;
this.lockedPricePerShare = args.lockedPricePerShare;
}
}
export class SafetyDepositBox {
/// Each token type in a vault has it's own box that contains it's mint and a look-back
key: VaultKey;
/// VaultKey pointing to the parent vault
vault: PublicKey;
/// This particular token's mint
tokenMint: PublicKey;
/// Account that stores the tokens under management
store: PublicKey;
/// the order in the array of registries
order: number;
constructor(args: {
vault: PublicKey;
tokenMint: PublicKey;
store: PublicKey;
order: number;
}) {
this.key = VaultKey.SafetyDepositBoxV1;
this.vault = args.vault;
this.tokenMint = args.tokenMint;
this.store = args.store;
this.order = args.order;
}
}
export class ExternalPriceAccount {
key: VaultKey;
pricePerShare: BN;
/// Mint of the currency we are pricing the shares against, should be same as redeem_treasury.
/// Most likely will be USDC mint most of the time.
priceMint: PublicKey;
/// Whether or not combination has been allowed for this vault.
allowedToCombine: boolean;
constructor(args: {
pricePerShare: BN;
priceMint: PublicKey;
allowedToCombine: boolean;
}) {
this.key = VaultKey.ExternalPriceAccountV1;
this.pricePerShare = args.pricePerShare;
this.priceMint = args.priceMint;
this.allowedToCombine = args.allowedToCombine;
}
}
class InitVaultArgs {
instruction: number = 0;
allowFurtherShareCreation: boolean = false;
constructor(args: { allowFurtherShareCreation: boolean }) {
this.allowFurtherShareCreation = args.allowFurtherShareCreation;
}
}
class AmountArgs {
instruction: number;
amount: BN;
constructor(args: { instruction: number; amount: BN }) {
this.instruction = args.instruction;
this.amount = args.amount;
}
}
class NumberOfShareArgs {
instruction: number;
numberOfShares: BN;
constructor(args: { instruction: number; numberOfShares: BN }) {
this.instruction = args.instruction;
this.numberOfShares = args.numberOfShares;
}
}
class UpdateExternalPriceAccountArgs {
instruction: number = 9;
externalPriceAccount: ExternalPriceAccount;
constructor(args: { externalPriceAccount: ExternalPriceAccount }) {
this.externalPriceAccount = args.externalPriceAccount;
}
}
export const VAULT_SCHEMA = new Map<any, any>([
[
InitVaultArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['allowFurtherShareCreation', 'u8'],
],
},
],
[
AmountArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['amount', 'u64'],
],
},
],
[
NumberOfShareArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['numberOfShares', 'u64'],
],
},
],
[
UpdateExternalPriceAccountArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['externalPriceAccount', ExternalPriceAccount],
],
},
],
[
Vault,
{
kind: 'struct',
fields: [
['key', 'u8'],
['tokenProgram', 'pubkey'],
['fractionMint', 'pubkey'],
['authority', 'pubkey'],
['fractionTreasury', 'pubkey'],
['redeemTreasury', 'pubkey'],
['allowFurtherShareCreation', 'u8'],
['pricingLookupAddress', 'u8'],
['tokenTypeCount', 'u8'],
['state', 'u8'],
['lockedPricePerShare', 'u64'],
],
},
],
[
SafetyDepositBox,
{
kind: 'struct',
fields: [
['key', 'u8'],
['vault', 'pubkey'],
['tokenMint', 'pubkey'],
['store', 'pubkey'],
['order', 'u8'],
],
},
],
[
ExternalPriceAccount,
{
kind: 'struct',
fields: [
['key', 'u8'],
['pricePerShare', 'u64'],
['priceMint', 'pubkey'],
['allowedToCombine', 'u8'],
],
},
],
]);
export const decodeVault = (buffer: Buffer) => {
return deserializeUnchecked(VAULT_SCHEMA, Vault, buffer) as Vault;
};
export const decodeSafetyDeposit = (buffer: Buffer) => {
return deserializeUnchecked(
VAULT_SCHEMA,
SafetyDepositBox,
buffer,
) as SafetyDepositBox;
};
export async function initVault(
allowFurtherShareCreation: boolean,
fractionalMint: PublicKey,
redeemTreasury: PublicKey,
fractionalTreasury: PublicKey,
vault: PublicKey,
vaultAuthority: PublicKey,
pricingLookupAddress: PublicKey,
instructions: TransactionInstruction[],
) {
const vaultProgramId = programIds().vault;
const data = Buffer.from(
serialize(VAULT_SCHEMA, new InitVaultArgs({ allowFurtherShareCreation })),
);
const keys = [
{
pubkey: fractionalMint,
isSigner: false,
isWritable: true,
},
{
pubkey: redeemTreasury,
isSigner: false,
isWritable: true,
},
{
pubkey: fractionalTreasury,
isSigner: false,
isWritable: true,
},
{
pubkey: vault,
isSigner: false,
isWritable: true,
},
{
pubkey: vaultAuthority,
isSigner: false,
isWritable: false,
},
{
pubkey: pricingLookupAddress,
isSigner: false,
isWritable: false,
},
{
pubkey: programIds().token,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: vaultProgramId,
data: data,
}),
);
}
export async function getSafetyDepositBox(
vault: PublicKey,
tokenMint: PublicKey,
): Promise<PublicKey> {
const vaultProgramId = programIds().vault;
return (
await PublicKey.findProgramAddress(
[Buffer.from(VAULT_PREFIX), vault.toBuffer(), tokenMint.toBuffer()],
vaultProgramId,
)
)[0];
}
export async function addTokenToInactiveVault(
amount: BN,
tokenMint: PublicKey,
tokenAccount: PublicKey,
tokenStoreAccount: PublicKey,
vault: PublicKey,
vaultAuthority: PublicKey,
payer: PublicKey,
transferAuthority: PublicKey,
instructions: TransactionInstruction[],
) {
const vaultProgramId = programIds().vault;
const safetyDepositBox: PublicKey = await getSafetyDepositBox(
vault,
tokenMint,
);
const value = new AmountArgs({
instruction: 1,
amount,
});
const data = Buffer.from(serialize(VAULT_SCHEMA, value));
const keys = [
{
pubkey: safetyDepositBox,
isSigner: false,
isWritable: true,
},
{
pubkey: tokenAccount,
isSigner: false,
isWritable: true,
},
{
pubkey: tokenStoreAccount,
isSigner: false,
isWritable: true,
},
{
pubkey: vault,
isSigner: false,
isWritable: true,
},
{
pubkey: vaultAuthority,
isSigner: true,
isWritable: false,
},
{
pubkey: payer,
isSigner: true,
isWritable: false,
},
{
pubkey: transferAuthority,
isSigner: true,
isWritable: false,
},
{
pubkey: programIds().token,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: vaultProgramId,
data,
}),
);
}
export async function activateVault(
numberOfShares: BN,
vault: PublicKey,
fractionMint: PublicKey,
fractionTreasury: PublicKey,
vaultAuthority: PublicKey,
instructions: TransactionInstruction[],
) {
const vaultProgramId = programIds().vault;
const fractionMintAuthority = (
await PublicKey.findProgramAddress(
[Buffer.from(VAULT_PREFIX), vaultProgramId.toBuffer()],
vaultProgramId,
)
)[0];
const value = new NumberOfShareArgs({ instruction: 2, numberOfShares });
const data = Buffer.from(serialize(VAULT_SCHEMA, value));
const keys = [
{
pubkey: vault,
isSigner: false,
isWritable: true,
},
{
pubkey: fractionMint,
isSigner: false,
isWritable: true,
},
{
pubkey: fractionTreasury,
isSigner: false,
isWritable: true,
},
{
pubkey: fractionMintAuthority,
isSigner: false,
isWritable: false,
},
{
pubkey: vaultAuthority,
isSigner: true,
isWritable: false,
},
{
pubkey: programIds().token,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: vaultProgramId,
data,
}),
);
}
export async function combineVault(
vault: PublicKey,
outstandingShareTokenAccount: PublicKey,
payingTokenAccount: PublicKey,
fractionMint: PublicKey,
fractionTreasury: PublicKey,
redeemTreasury: PublicKey,
newVaultAuthority: PublicKey | undefined,
vaultAuthority: PublicKey,
transferAuthority: PublicKey,
externalPriceAccount: PublicKey,
instructions: TransactionInstruction[],
) {
const vaultProgramId = programIds().vault;
const burnAuthority = (
await PublicKey.findProgramAddress(
[Buffer.from(VAULT_PREFIX), vaultProgramId.toBuffer()],
vaultProgramId,
)
)[0];
const data = Buffer.from([3]);
const keys = [
{
pubkey: vault,
isSigner: false,
isWritable: true,
},
{
pubkey: outstandingShareTokenAccount,
isSigner: false,
isWritable: true,
},
{
pubkey: payingTokenAccount,
isSigner: false,
isWritable: true,
},
{
pubkey: fractionMint,
isSigner: false,
isWritable: true,
},
{
pubkey: fractionTreasury,
isSigner: false,
isWritable: true,
},
{
pubkey: redeemTreasury,
isSigner: false,
isWritable: true,
},
{
pubkey: newVaultAuthority || vaultAuthority,
isSigner: false,
isWritable: false,
},
{
pubkey: vaultAuthority,
isSigner: true,
isWritable: false,
},
{
pubkey: transferAuthority,
isSigner: true,
isWritable: false,
},
{
pubkey: burnAuthority,
isSigner: false,
isWritable: false,
},
{
pubkey: externalPriceAccount,
isSigner: false,
isWritable: false,
},
{
pubkey: programIds().token,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: vaultProgramId,
data,
}),
);
}
export async function withdrawTokenFromSafetyDepositBox(
amount: BN,
destination: PublicKey,
safetyDepositBox: PublicKey,
storeKey: PublicKey,
vault: PublicKey,
fractionMint: PublicKey,
vaultAuthority: PublicKey,
instructions: TransactionInstruction[],
) {
const vaultProgramId = programIds().vault;
const transferAuthority = (
await PublicKey.findProgramAddress(
[Buffer.from(VAULT_PREFIX), vaultProgramId.toBuffer()],
vaultProgramId,
)
)[0];
const value = new AmountArgs({ instruction: 5, amount });
const data = Buffer.from(serialize(VAULT_SCHEMA, value));
const keys = [
{
pubkey: destination,
isSigner: false,
isWritable: true,
},
{
pubkey: safetyDepositBox,
isSigner: false,
isWritable: true,
},
{
pubkey: storeKey,
isSigner: false,
isWritable: true,
},
{
pubkey: vault,
isSigner: false,
isWritable: true,
},
{
pubkey: fractionMint,
isSigner: false,
isWritable: true,
},
{
pubkey: vaultAuthority,
isSigner: true,
isWritable: false,
},
{
pubkey: transferAuthority,
isSigner: false,
isWritable: false,
},
{
pubkey: programIds().token,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: vaultProgramId,
data,
}),
);
}
export async function updateExternalPriceAccount(
externalPriceAccountKey: PublicKey,
externalPriceAccount: ExternalPriceAccount,
instructions: TransactionInstruction[],
) {
const vaultProgramId = programIds().vault;
const value = new UpdateExternalPriceAccountArgs({ externalPriceAccount });
const data = Buffer.from(serialize(VAULT_SCHEMA, value));
console.log('Data', data);
const keys = [
{
pubkey: externalPriceAccountKey,
isSigner: false,
isWritable: true,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: vaultProgramId,
data,
}),
);
}
export async function getSafetyDepositBoxAddress(
vault: PublicKey,
tokenMint: PublicKey,
): Promise<PublicKey> {
const PROGRAM_IDS = programIds();
return (
await PublicKey.findProgramAddress(
[Buffer.from(VAULT_PREFIX), vault.toBuffer(), tokenMint.toBuffer()],
PROGRAM_IDS.vault,
)
)[0];
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Button } from 'antd';
import { LABELS } from '../../constants';
import { Link } from 'react-router-dom';
import './style.css';
export const ActionConfirmation = (props: {
className?: string;
onClose: () => void;
}) => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-around',
alignItems: 'center',
}}
>
<h2>Congratulations!</h2>
<div>Your action has been successfully executed</div>
<div className="success-icon" />
<Link to="/dashboard">
<Button type="primary">{LABELS.DASHBOARD_ACTION}</Button>
</Link>
<Button type="text" onClick={props.onClose}>
{LABELS.GO_BACK_ACTION}
</Button>
</div>
);
};

View File

@ -0,0 +1,5 @@
.success-icon {
background-image: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjNzBjMDQxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDY0IDY0IiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPmJ1c2luZXNzIGZpbmFuY2UgdXAgcmlzZSBhcnJvdyBkZW1hbmQ8L3RpdGxlPjxwYXRoIGQ9Ik01LDE0YTEsMSwwLDEsMC0xLTFINEExLDEsMCwwLDAsNSwxNFoiPjwvcGF0aD48cGF0aCBkPSJNNyw1Mkg1NWExLDEsMCwwLDAsMC0ySDUwVjI3YTEsMSwwLDAsMC0yLDBWNTBINDRWMzRhMSwxLDAsMCwwLTIsMFY1MEgzOFYzOWExLDEsMCwwLDAtMiwwVjUwSDMyVjQzYTEsMSwwLDAsMC0yLDB2N0gyNlY0NmExLDEsMCwwLDAtMiwwdjRIMjBWNDdhMSwxLDAsMCwwLTIsMHYzSDE0VjQ4YTEsMSwwLDAsMC0yLDB2Mkg3YTEsMSwwLDAsMS0xLTFWMTdhMSwxLDAsMCwwLTIsMFY0OUEzLDMsMCwwLDAsNyw1MloiPjwvcGF0aD48cGF0aCBkPSJNNTksNTBhMSwxLDAsMCwwLTEsMWgwYTEsMSwwLDEsMCwxLTFaIj48L3BhdGg+PHBhdGggZD0iTTExLDQ0aC4wN2E0OS4wNyw0OS4wNywwLDAsMCwyNS41Mi05LjE5QTQ4LjkxLDQ4LjkxLDAsMCwwLDQ5LjcsMjAuNTlMNTIuMzgsMjIsNTIsMTRsLTYuNzEsNC4zMSwyLjYzLDEuMzZBNDYuODEsNDYuODEsMCwwLDEsMzUuNDEsMzMuMTksNDYuOTQsNDYuOTQsMCwwLDEsMTAuOTMsNDIsMSwxLDAsMCwwLDExLDQ0WiI+PC9wYXRoPjwvc3ZnPg==');
width: 280px;
height: 280px;
}

View File

@ -0,0 +1,51 @@
import React from 'react';
import { Button, Popover } from 'antd';
import { CurrentUserBadge } from '../CurrentUserBadge';
import { SettingOutlined } from '@ant-design/icons';
import { Settings } from '../Settings';
import { LABELS } from '../../constants/labels';
import { ConnectButton } from '..';
import { useWallet } from '../../contexts/wallet';
import './style.css';
export const AppBar = (props: {
left?: JSX.Element;
right?: JSX.Element;
useWalletBadge?: boolean;
additionalSettings?: JSX.Element;
}) => {
const { connected } = useWallet();
const TopBar = (
<div className="App-Bar-right">
{props.left}
{connected ?
(
<CurrentUserBadge />
)
: (
<ConnectButton
type="text"
size="large"
allowWalletChange={true}
style={{ color: '#2abdd2' }}
/>
)}
<Popover
placement="topRight"
title={LABELS.SETTINGS_TOOLTIP}
content={<Settings additionalSettings={props.additionalSettings} />}
trigger="click"
>
<Button
shape="circle"
size="large"
type="text"
icon={<SettingOutlined />}
/>
</Popover>
{props.right}
</div>
);
return TopBar;
};

View File

@ -0,0 +1,57 @@
.App-Bar {
-webkit-box-pack: justify;
justify-content: space-between !important;
-webkit-box-align: center;
align-items: center;
flex-direction: row;
display: flex;
width: 100%;
top: 0px;
position: relative;
padding: 1rem;
z-index: 2;
.ant-menu-horizontal {
border-bottom-color: transparent;
background-color: transparent;
line-height: inherit;
font-size: 16px;
margin: 0 10px;
.ant-menu-item {
margin: 0 10px;
color: lightgrey;
height: 35px;
line-height: 35px;
border-width: 0px !important;
}
.ant-menu-item:hover {
color: white;
border-width: 0px !important;
}
.ant-menu-item-selected {
font-weight: bold;
}
}
}
.App-Bar-left {
box-sizing: border-box;
margin: 0px;
min-width: 0px;
display: flex;
padding: 0px;
-webkit-box-align: center;
align-items: center;
width: fit-content;
}
.App-Bar-right {
display: flex;
flex-direction: row;
-webkit-box-align: center;
align-items: center;
justify-self: flex-end;
}

View File

@ -0,0 +1,13 @@
import React from 'react';
import { Button } from 'antd';
import { LABELS } from '../../constants';
import { useHistory } from 'react-router-dom';
export const BackButton = () => {
const history = useHistory();
return (
<Button type="text" onClick={history.goBack}>
{LABELS.GO_BACK_ACTION}
</Button>
);
};

View File

@ -0,0 +1,43 @@
import { Button, Dropdown, Menu } from "antd";
import { ButtonProps } from "antd/lib/button";
import React from "react";
import { useWallet } from './../../contexts/wallet';
export interface ConnectButtonProps extends ButtonProps, React.RefAttributes<HTMLElement> {
allowWalletChange?: boolean;
}
export const ConnectButton = (
props: ConnectButtonProps
) => {
const { connected, connect, select, provider } = useWallet();
const { onClick, children, disabled, allowWalletChange, ...rest } = props;
// only show if wallet selected or user connected
const menu = (
<Menu>
<Menu.Item key="3" onClick={select}>Change Wallet</Menu.Item>
</Menu>
);
if(!provider || !allowWalletChange) {
return <Button
className="connector"
{...rest}
onClick={connected ? onClick : connect}
disabled={connected && disabled}
>
{connected ? children : 'Connect'}
</Button>;
}
return (
<Dropdown.Button
onClick={connected ? onClick : connect}
disabled={connected && disabled}
overlay={menu}>
Connect
</Dropdown.Button>
);
};

View File

@ -0,0 +1,73 @@
import React from 'react';
import { Identicon } from '../Identicon';
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
import { useWallet } from '../../contexts/wallet';
import { useNativeAccount } from '../../contexts/accounts';
import { formatNumber, shortenAddress } from '../../utils';
import './styles.css';
import { Popover } from 'antd';
import { Settings } from '../Settings';
export const CurrentUserBadge = (props: { showBalance?: boolean, showAddress?: boolean, iconSize?: number }) => {
const { wallet } = useWallet();
const { account } = useNativeAccount();
if (!wallet || !wallet.publicKey) {
return null;
}
const iconStyle: React.CSSProperties = props.showAddress ?
{
marginLeft: '0.5rem',
display: 'flex',
width: props.iconSize,
borderRadius: 50,
} :{
display: 'flex',
width: props.iconSize,
paddingLeft: 0,
borderRadius: 50,
};
const baseWalletKey: React.CSSProperties = { height: props.iconSize, cursor: 'pointer', userSelect: 'none' };
const walletKeyStyle: React.CSSProperties = props.showAddress ?
baseWalletKey
:{ ...baseWalletKey, paddingLeft: 0 };
let name = props.showAddress ? shortenAddress(`${wallet.publicKey}`) : '';
const unknownWallet = wallet as any;
if(unknownWallet.name) {
name = unknownWallet.name;
}
let image = <Identicon
address={wallet.publicKey?.toBase58()}
style={iconStyle}
/>;
if(unknownWallet.image) {
image = <img src={unknownWallet.image} style={iconStyle} />;
}
return (
<div className="wallet-wrapper">
{props.showBalance && <span>
{formatNumber.format((account?.lamports || 0) / LAMPORTS_PER_SOL)} SOL
</span>}
<Popover
placement="topRight"
title="Settings"
content={<Settings />}
trigger="click"
>
<div className="wallet-key" style={walletKeyStyle}>
{name && (<span style={{ marginRight: '0.5rem' }}>{name}</span>)}
{image}
</div>
</Popover>
</div>
);
};

View File

@ -0,0 +1,15 @@
.wallet-wrapper {
padding-left: 0.7rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
white-space: nowrap;
}
.wallet-key {
padding: 0.1rem 0.5rem 0.1rem 0.7rem;
margin-left: 0.3rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
}

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Typography } from 'antd';
import { shortenAddress } from '../../utils/utils';
export const EtherscanLink = (props: {
address: string ;
type: string;
code?: boolean;
style?: React.CSSProperties;
length?: number;
}) => {
const { type, code } = props;
const address = props.address;
if (!address) {
return null;
}
const length = props.length ?? 9;
return (
<a
href={`https://etherscan.io/${type}/${address}`}
// eslint-disable-next-line react/jsx-no-target-blank
target="_blank"
title={address}
style={props.style}
>
{code ? (
<Typography.Text style={props.style} code>
{shortenAddress(address, length)}
</Typography.Text>
) : (
shortenAddress(address, length)
)}
</a>
);
};

View File

@ -0,0 +1,43 @@
import React from 'react';
import { Typography } from 'antd';
import { shortenAddress } from '../../utils/utils';
import { PublicKey } from '@solana/web3.js';
export const ExplorerLink = (props: {
address: string | PublicKey;
type: string;
code?: boolean;
style?: React.CSSProperties;
length?: number;
}) => {
const { type, code } = props;
const address =
typeof props.address === 'string'
? props.address
: props.address?.toBase58();
if (!address) {
return null;
}
const length = props.length ?? 9;
return (
<a
href={`https://explorer.solana.com/${type}/${address}`}
// eslint-disable-next-line react/jsx-no-target-blank
target="_blank"
title={address}
style={props.style}
>
{code ? (
<Typography.Text style={props.style} code>
{shortenAddress(address, length)}
</Typography.Text>
) : (
shortenAddress(address, length)
)}
</a>
);
};

View File

@ -0,0 +1,20 @@
import { Button, Popover } from "antd";
import React from "react";
import { InfoCircleOutlined } from "@ant-design/icons";
export const Info = (props: {
text: React.ReactElement;
style?: React.CSSProperties;
}) => {
return (
<Popover
trigger="hover"
content={<div style={{ width: 300 }}>{props.text}</div>}
>
<Button type="text" shape="circle">
<InfoCircleOutlined style={props.style} />
</Button>
</Popover>
);
};

View File

@ -0,0 +1,43 @@
import React, { useEffect, useRef } from 'react';
import Jazzicon from 'jazzicon';
import bs58 from 'bs58';
import './style.css';
import { PublicKey } from '@solana/web3.js';
export const Identicon = (props: {
address?: string | PublicKey;
style?: React.CSSProperties;
className?: string;
alt?: string;
}) => {
const { style, className, alt } = props;
const address =
typeof props.address === 'string'
? props.address
: props.address?.toBase58();
const ref = useRef<HTMLDivElement>();
useEffect(() => {
if (address && ref.current) {
try {
ref.current.innerHTML = '';
ref.current.className = className || '';
ref.current.appendChild(
Jazzicon(
style?.width || 16,
parseInt(bs58.decode(address).toString('hex').slice(5, 15), 16),
),
);
} catch (err) {
// TODO
}
}
}, [address, style, className]);
return (
<div className="identicon-wrapper" title={alt} ref={ref as any} style={props.style} />
);
};

View File

@ -0,0 +1,8 @@
.identicon-wrapper {
display: flex;
height: 1rem;
width: 1rem;
border-radius: 1.125rem;
margin: 0.2rem 0.2rem 0.2rem 0.1rem;
/* background-color: ${({ theme }) => theme.bg4}; */
}

View File

@ -0,0 +1,43 @@
import React from "react";
import { Input } from "antd";
export class NumericInput extends React.Component<any, any> {
onChange = (e: any) => {
const { value } = e.target;
const reg = /^-?\d*(\.\d*)?$/;
if (reg.test(value) || value === "" || value === "-") {
this.props.onChange(value);
}
};
// '.' at the end or only '-' in the input box.
onBlur = () => {
const { value, onBlur, onChange } = this.props;
let valueTemp = value;
if (value === undefined || value === null) return;
if (
value.charAt &&
(value.charAt(value.length - 1) === "." || value === "-")
) {
valueTemp = value.slice(0, -1);
}
if (value.startsWith && (value.startsWith(".") || value.startsWith("-."))) {
valueTemp = valueTemp.replace(".", "0.");
}
if (valueTemp.replace) onChange?.(valueTemp.replace(/0*(\d+)/, "$1"));
if (onBlur) {
onBlur();
}
};
render() {
return (
<Input
{...this.props}
onChange={this.onChange}
onBlur={this.onBlur}
maxLength={25}
/>
);
}
}

View File

@ -0,0 +1,9 @@
.ant-scrolling-effect {
#root>.ant-layout {
filter: blur(10px) brightness(0.5);
}
}
.ant-modal-content {
border-radius: 16px;
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Modal } from 'antd';
import './index.css';
export const MetaplexModal = (props: any) => {
const { children, bodyStyle, ...rest } = props
return (
<Modal
bodyStyle={{
background: "#2F2F2F",
boxShadow: '0px 6px 12px 8px rgba(0, 0, 0, 0.3)',
borderRadius: 16,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
...bodyStyle,
}}
footer={null}
width={400}
{...rest}
>
{children}
</Modal>
);
};

View File

@ -0,0 +1,27 @@
import React from 'react';
import { Modal } from 'antd';
export const MetaplexOverlay = (props: any) => {
const { children, ...rest } = props
const content = <div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
pointerEvents: "auto",
justifyContent: "center"
}}>
{children}
</div>
return (
<Modal
centered
modalRender={() => content}
width={'100vw'}
mask={false}
{...rest}
></Modal>
);
};

View File

@ -0,0 +1,64 @@
import React from 'react';
import { Button, Select } from 'antd';
import { useWallet } from '../../contexts/wallet';
import { ENDPOINTS, useConnectionConfig } from '../../contexts/connection';
import { shortenAddress } from '../../utils';
import { CopyOutlined } from '@ant-design/icons';
export const Settings = ({
additionalSettings,
}: {
additionalSettings?: JSX.Element;
}) => {
const { connected, disconnect, select, wallet } = useWallet();
const { endpoint, setEndpoint } = useConnectionConfig();
return (
<>
<div style={{ display: 'grid' }}>
Network:{' '}
<Select
onSelect={setEndpoint}
value={endpoint}
style={{ marginBottom: 20 }}
>
{ENDPOINTS.map(({ name, endpoint }) => (
<Select.Option value={endpoint} key={endpoint}>
{name}
</Select.Option>
))}
</Select>
{connected && (
<>
<span>Wallet:</span>
{wallet?.publicKey && (
<Button
style={{ marginBottom: 5 }}
onClick={() =>
navigator.clipboard.writeText(
wallet.publicKey?.toBase58() || '',
)
}
>
<CopyOutlined />
{shortenAddress(wallet?.publicKey.toBase58())}
</Button>
)}
<Button onClick={select} style={{ marginBottom: 5 }}>
Change
</Button>
<Button
type="primary"
onClick={disconnect}
style={{ marginBottom: 5 }}
>
Disconnect
</Button>
</>
)}
{additionalSettings}
</div>
</>
);
};

View File

@ -0,0 +1,58 @@
import React from 'react';
import { useMint } from '../../contexts/accounts';
import { useAccountByMint } from '../../hooks';
import { TokenIcon } from '../TokenIcon';
export const TokenDisplay = (props: {
name: string;
mintAddress: string;
icon?: JSX.Element;
showBalance?: boolean;
}) => {
const { showBalance, mintAddress, name, icon } = props;
const tokenMint = useMint(mintAddress);
const tokenAccount = useAccountByMint(mintAddress);
let balance: number = 0;
let hasBalance: boolean = false;
if (showBalance) {
if (tokenAccount && tokenMint) {
balance =
tokenAccount.info.amount.toNumber() / Math.pow(10, tokenMint.decimals);
hasBalance = balance > 0;
}
}
return (
<>
<div
title={mintAddress}
key={mintAddress}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
{icon || <TokenIcon mintAddress={mintAddress} />}
{name}
</div>
{showBalance ? (
<span
title={balance.toString()}
key={mintAddress}
className="token-balance"
>
&nbsp;{' '}
{hasBalance
? balance < 0.001
? '<0.001'
: balance.toFixed(3)
: '-'}
</span>
) : null}
</div>
</>
);
};

View File

@ -0,0 +1,73 @@
import React from 'react';
import { PublicKey } from '@solana/web3.js';
import {getTokenIcon, KnownTokenMap} from '../../utils';
import { useConnectionConfig } from '../../contexts/connection';
import { Identicon } from '../Identicon';
export const TokenIcon = (props: {
mintAddress?: string | PublicKey;
style?: React.CSSProperties;
size?: number;
className?: string;
tokenMap?: KnownTokenMap,
}) => {
let icon: string | undefined = '';
if (props.tokenMap) {
icon = getTokenIcon(props.tokenMap, props.mintAddress);
} else {
const { tokenMap } = useConnectionConfig();
icon = getTokenIcon(tokenMap, props.mintAddress);
}
const size = props.size || 20;
if (icon) {
return (
<img
alt="Token icon"
className={props.className}
key={icon}
width={props.style?.width || size.toString()}
height={props.style?.height || size.toString()}
src={icon}
style={{
marginRight: '0.5rem',
marginTop: '0.11rem',
borderRadius: '10rem',
backgroundColor: 'white',
backgroundClip: 'padding-box',
...props.style,
}}
/>
);
}
return (
<Identicon
address={props.mintAddress}
style={{
marginRight: '0.5rem',
width: size,
height: size,
marginTop: 2,
...props.style,
}}
/>
);
};
export const PoolIcon = (props: {
mintA: string;
mintB: string;
style?: React.CSSProperties;
className?: string;
}) => {
return (
<div className={props.className} style={{ display: 'flex' }}>
<TokenIcon
mintAddress={props.mintA}
style={{ marginRight: '-0.5rem', ...props.style }}
/>
<TokenIcon mintAddress={props.mintB} />
</div>
);
};

View File

@ -0,0 +1,15 @@
export { ExplorerLink } from './ExplorerLink/index';
export { ConnectButton } from './ConnectButton/index';
export { CurrentUserBadge } from './CurrentUserBadge/index';
export { Identicon } from './Identicon/index';
export { Info } from './Icons/info';
export { NumericInput } from './Input/numeric';
export { AppBar } from './AppBar/index';
export { Settings } from './Settings/index';
export { ActionConfirmation } from './ActionConfirmation/index';
export { BackButton } from './BackButton/index';
export { TokenIcon } from './TokenIcon';
export { TokenDisplay } from './TokenDisplay';
export { EtherscanLink } from './EtherscanLink';
export { MetaplexModal } from './MetaplexModal';
export { MetaplexOverlay } from './MetaplexOverlay';

View File

@ -0,0 +1,2 @@
export * from './math';
export * from './labels';

View File

@ -0,0 +1,15 @@
export const LABELS = {
CONNECT_LABEL: 'Connect Wallet',
AUDIT_WARNING:
'Oyster is an unaudited software project used for internal purposes at the Solana Foundation. This app is not for public use.',
FOOTER:
'This page was produced by the Solana Foundation ("SF") for internal educational and inspiration purposes only. SF does not encourage, induce or sanction the deployment, integration or use of Oyster or any similar application (including its code) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. Anyone using this code or a derivation thereof must comply with applicable laws and regulations when releasing related software.',
MENU_HOME: 'Home',
MENU_DASHBOARD: 'Dashboard',
CONNECT_BUTTON: 'Connect',
WALLET_TOOLTIP: 'Wallet public key',
WALLET_BALANCE: 'Wallet balance',
SETTINGS_TOOLTIP: 'Settings',
DASHBOARD_ACTION: 'Go to dashboard',
GO_BACK_ACTION: 'Go back',
};

View File

@ -0,0 +1,7 @@
import BN from 'bn.js';
export const TEN = new BN(10);
export const HALF_WAD = TEN.pow(new BN(18));
export const WAD = TEN.pow(new BN(18));
export const RAY = TEN.pow(new BN(27));
export const ZERO = new BN(0);

View File

@ -0,0 +1,664 @@
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { useConnection } from '../contexts/connection';
import { useWallet } from '../contexts/wallet';
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import { AccountLayout, MintInfo, MintLayout, u64 } from '@solana/spl-token';
import { TokenAccount } from '../models';
import { chunks } from '../utils/utils';
import { EventEmitter } from '../utils/eventEmitter';
import { useUserAccounts } from '../hooks/useUserAccounts';
import {
WRAPPED_SOL_MINT,
programIds,
LEND_HOST_FEE_ADDRESS,
} from '../utils/ids';
const AccountsContext = React.createContext<any>(null);
const pendingCalls = new Map<string, Promise<ParsedAccountBase>>();
const genericCache = new Map<string, ParsedAccountBase>();
const pendingMintCalls = new Map<string, Promise<MintInfo>>();
const mintCache = new Map<string, MintInfo>();
export interface ParsedAccountBase {
pubkey: PublicKey;
account: AccountInfo<Buffer>;
info: any; // TODO: change to unkown
}
export type AccountParser = (
pubkey: PublicKey,
data: AccountInfo<Buffer>,
) => ParsedAccountBase | undefined;
export interface ParsedAccount<T> extends ParsedAccountBase {
info: T;
}
const getMintInfo = async (connection: Connection, pubKey: PublicKey) => {
const info = await connection.getAccountInfo(pubKey);
if (info === null) {
throw new Error('Failed to find mint account');
}
const data = Buffer.from(info.data);
return deserializeMint(data);
};
export const MintParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
const buffer = Buffer.from(info.data);
const data = deserializeMint(buffer);
const details = {
pubkey: pubKey,
account: {
...info,
},
info: data,
} as ParsedAccountBase;
return details;
};
export const TokenAccountParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
) => {
const buffer = Buffer.from(info.data);
const data = deserializeAccount(buffer);
const details = {
pubkey: pubKey,
account: {
...info,
},
info: data,
} as TokenAccount;
return details;
};
export const GenericAccountParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
) => {
const buffer = Buffer.from(info.data);
const details = {
pubkey: pubKey,
account: {
...info,
},
info: buffer,
} as ParsedAccountBase;
return details;
};
export const keyToAccountParser = new Map<string, AccountParser>();
export const cache = {
emitter: new EventEmitter(),
query: async (
connection: Connection,
pubKey: string | PublicKey,
parser?: AccountParser,
) => {
let id: PublicKey;
if (typeof pubKey === 'string') {
id = new PublicKey(pubKey);
} else {
id = pubKey;
}
const address = id.toBase58();
let account = genericCache.get(address);
if (account) {
return account;
}
let query = pendingCalls.get(address);
if (query) {
return query;
}
// TODO: refactor to use multiple accounts query with flush like behavior
query = connection.getAccountInfo(id).then(data => {
if (!data) {
throw new Error('Account not found');
}
return cache.add(id, data, parser);
}) as Promise<TokenAccount>;
pendingCalls.set(address, query as any);
return query;
},
add: (
id: PublicKey | string,
obj: AccountInfo<Buffer>,
parser?: AccountParser,
) => {
if (obj.data.length === 0) {
return;
}
const address = typeof id === 'string' ? id : id?.toBase58();
const deserialize = parser ? parser : keyToAccountParser.get(address);
if (!deserialize) {
throw new Error(
'Deserializer needs to be registered or passed as a parameter',
);
}
cache.registerParser(id, deserialize);
pendingCalls.delete(address);
const account = deserialize(new PublicKey(address), obj);
if (!account) {
return;
}
const isNew = !genericCache.has(address);
genericCache.set(address, account);
cache.emitter.raiseCacheUpdated(address, isNew, deserialize);
return account;
},
get: (pubKey: string | PublicKey) => {
let key: string;
if (typeof pubKey !== 'string') {
key = pubKey.toBase58();
} else {
key = pubKey;
}
return genericCache.get(key);
},
delete: (pubKey: string | PublicKey) => {
let key: string;
if (typeof pubKey !== 'string') {
key = pubKey.toBase58();
} else {
key = pubKey;
}
if (genericCache.get(key)) {
genericCache.delete(key);
cache.emitter.raiseCacheDeleted(key);
return true;
}
return false;
},
byParser: (parser: AccountParser) => {
const result: string[] = [];
for (const id of keyToAccountParser.keys()) {
if (keyToAccountParser.get(id) === parser) {
result.push(id);
}
}
return result;
},
registerParser: (pubkey: PublicKey | string, parser: AccountParser) => {
if (pubkey) {
const address = typeof pubkey === 'string' ? pubkey : pubkey?.toBase58();
keyToAccountParser.set(address, parser);
}
return pubkey;
},
queryMint: async (connection: Connection, pubKey: string | PublicKey) => {
let id: PublicKey;
if (typeof pubKey === 'string') {
id = new PublicKey(pubKey);
} else {
id = pubKey;
}
const address = id.toBase58();
let mint = mintCache.get(address);
if (mint) {
return mint;
}
let query = pendingMintCalls.get(address);
if (query) {
return query;
}
query = getMintInfo(connection, id).then(data => {
pendingMintCalls.delete(address);
mintCache.set(address, data);
return data;
}) as Promise<MintInfo>;
pendingMintCalls.set(address, query as any);
return query;
},
getMint: (pubKey: string | PublicKey) => {
let key: string;
if (typeof pubKey !== 'string') {
key = pubKey.toBase58();
} else {
key = pubKey;
}
return mintCache.get(key);
},
addMint: (pubKey: PublicKey, obj: AccountInfo<Buffer>) => {
const mint = deserializeMint(obj.data);
const id = pubKey.toBase58();
mintCache.set(id, mint);
return mint;
},
};
export const useAccountsContext = () => {
const context = useContext(AccountsContext);
return context;
};
function wrapNativeAccount(
pubkey: PublicKey,
account?: AccountInfo<Buffer>,
): TokenAccount | undefined {
if (!account) {
return undefined;
}
return {
pubkey: pubkey,
account,
info: {
address: pubkey,
mint: WRAPPED_SOL_MINT,
owner: pubkey,
amount: new u64(account.lamports),
delegate: null,
delegatedAmount: new u64(0),
isInitialized: true,
isFrozen: false,
isNative: true,
rentExemptReserve: null,
closeAuthority: null,
},
};
}
export const getCachedAccount = (
predicate: (account: TokenAccount) => boolean,
) => {
for (const account of genericCache.values()) {
if (predicate(account)) {
return account as TokenAccount;
}
}
};
const UseNativeAccount = () => {
const connection = useConnection();
const { wallet } = useWallet();
const [nativeAccount, setNativeAccount] = useState<AccountInfo<Buffer>>();
const updateCache = useCallback(
account => {
if (wallet && wallet.publicKey) {
const wrapped = wrapNativeAccount(wallet.publicKey, account);
if (wrapped !== undefined && wallet) {
const id = wallet.publicKey?.toBase58();
cache.registerParser(id, TokenAccountParser);
genericCache.set(id, wrapped as TokenAccount);
cache.emitter.raiseCacheUpdated(id, false, TokenAccountParser);
}
}
},
[wallet],
);
useEffect(() => {
let subId = 0;
const updateAccount = (account: AccountInfo<Buffer> | null) => {
if (account) {
updateCache(account);
setNativeAccount(account);
}
};
(async () => {
if (!connection || !wallet?.publicKey) {
return;
}
const account = await connection.getAccountInfo(wallet.publicKey)
updateAccount(account);
subId = connection.onAccountChange(wallet.publicKey, updateAccount);
})();
return () => {
if (subId) {
connection.removeAccountChangeListener(subId);
}
}
}, [setNativeAccount, wallet, wallet?.publicKey, connection, updateCache]);
return { nativeAccount };
};
const PRECACHED_OWNERS = new Set<string>();
const precacheUserTokenAccounts = async (
connection: Connection,
owner?: PublicKey,
) => {
if (!owner) {
return;
}
// used for filtering account updates over websocket
PRECACHED_OWNERS.add(owner.toBase58());
// user accounts are updated via ws subscription
const accounts = await connection.getTokenAccountsByOwner(owner, {
programId: programIds().token,
});
accounts.value.forEach(info => {
cache.add(info.pubkey.toBase58(), info.account, TokenAccountParser);
});
};
export function AccountsProvider({ children = null as any }) {
const connection = useConnection();
const { wallet, connected } = useWallet();
const [tokenAccounts, setTokenAccounts] = useState<TokenAccount[]>([]);
const [userAccounts, setUserAccounts] = useState<TokenAccount[]>([]);
const { nativeAccount } = UseNativeAccount();
const selectUserAccounts = useCallback(() => {
return cache
.byParser(TokenAccountParser)
.map(id => cache.get(id))
.filter(
a => a && a.info.owner.toBase58() === wallet?.publicKey?.toBase58(),
)
.map(a => a as TokenAccount);
}, [wallet]);
useEffect(() => {
const accounts = selectUserAccounts().filter(
a => a !== undefined,
) as TokenAccount[];
setUserAccounts(accounts);
}, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]);
useEffect(() => {
const subs: number[] = [];
cache.emitter.onCache(args => {
if (args.isNew) {
let id = args.id;
let deserialize = args.parser;
connection.onAccountChange(new PublicKey(id), info => {
cache.add(id, info, deserialize);
});
}
});
return () => {
subs.forEach(id => connection.removeAccountChangeListener(id));
};
}, [connection]);
const publicKey = wallet?.publicKey;
useEffect(() => {
if (!connection || !publicKey) {
setTokenAccounts([]);
} else {
precacheUserTokenAccounts(connection, LEND_HOST_FEE_ADDRESS);
precacheUserTokenAccounts(connection, publicKey).then(() => {
setTokenAccounts(selectUserAccounts());
});
// This can return different types of accounts: token-account, mint, multisig
// TODO: web3.js expose ability to filter.
// this should use only filter syntax to only get accounts that are owned by user
const tokenSubID = connection.onProgramAccountChange(
programIds().token,
info => {
// TODO: fix type in web3.js
const id = (info.accountId as unknown) as string;
// TODO: do we need a better way to identify layout (maybe a enum identifing type?)
if (info.accountInfo.data.length === AccountLayout.span) {
const data = deserializeAccount(info.accountInfo.data);
if (PRECACHED_OWNERS.has(data.owner.toBase58())) {
cache.add(id, info.accountInfo, TokenAccountParser);
setTokenAccounts(selectUserAccounts());
}
}
},
'singleGossip',
);
return () => {
connection.removeProgramAccountChangeListener(tokenSubID);
};
}
}, [connection, connected, publicKey, selectUserAccounts]);
return (
<AccountsContext.Provider
value={{
userAccounts,
nativeAccount,
}}
>
{children}
</AccountsContext.Provider>
);
}
export function useNativeAccount() {
const context = useContext(AccountsContext);
return {
account: context.nativeAccount as AccountInfo<Buffer>,
};
}
export const getMultipleAccounts = async (
connection: any,
keys: string[],
commitment: string,
) => {
const result = await Promise.all(
chunks(keys, 99).map(chunk =>
getMultipleAccountsCore(connection, chunk, commitment),
),
);
const array = result
.map(
a =>
a.array.map(acc => {
if (!acc) {
return undefined;
}
const { data, ...rest } = acc;
const obj = {
...rest,
data: Buffer.from(data[0], 'base64'),
} as AccountInfo<Buffer>;
return obj;
}) as AccountInfo<Buffer>[],
)
.flat();
return { keys, array };
};
const getMultipleAccountsCore = async (
connection: any,
keys: string[],
commitment: string,
) => {
const args = connection._buildArgs([keys], commitment, 'base64');
const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args);
if (unsafeRes.error) {
throw new Error(
'failed to get info about account ' + unsafeRes.error.message,
);
}
if (unsafeRes.result.value) {
const array = unsafeRes.result.value as AccountInfo<string[]>[];
return { keys, array };
}
// TODO: fix
throw new Error();
};
export function useMint(key?: string | PublicKey) {
const connection = useConnection();
const [mint, setMint] = useState<MintInfo>();
const id = typeof key === 'string' ? key : key?.toBase58();
useEffect(() => {
if (!id) {
return;
}
cache
.query(connection, id, MintParser)
.then(acc => setMint(acc.info as any))
.catch(err => console.log(err));
const dispose = cache.emitter.onCache(e => {
const event = e;
if (event.id === id) {
cache
.query(connection, id, MintParser)
.then(mint => setMint(mint.info as any));
}
});
return () => {
dispose();
};
}, [connection, id]);
return mint;
}
export function useAccount(pubKey?: PublicKey) {
const connection = useConnection();
const [account, setAccount] = useState<TokenAccount>();
const key = pubKey?.toBase58();
useEffect(() => {
const query = async () => {
try {
if (!key) {
return;
}
const acc = await cache
.query(connection, key, TokenAccountParser)
.catch(err => console.log(err));
if (acc) {
setAccount(acc);
}
} catch (err) {
console.error(err);
}
};
query();
const dispose = cache.emitter.onCache(e => {
const event = e;
if (event.id === key) {
query();
}
});
return () => {
dispose();
};
}, [connection, key]);
return account;
}
// TODO: expose in spl package
export const deserializeAccount = (data: Buffer) => {
const accountInfo = AccountLayout.decode(data);
accountInfo.mint = new PublicKey(accountInfo.mint);
accountInfo.owner = new PublicKey(accountInfo.owner);
accountInfo.amount = u64.fromBuffer(accountInfo.amount);
if (accountInfo.delegateOption === 0) {
accountInfo.delegate = null;
accountInfo.delegatedAmount = new u64(0);
} else {
accountInfo.delegate = new PublicKey(accountInfo.delegate);
accountInfo.delegatedAmount = u64.fromBuffer(accountInfo.delegatedAmount);
}
accountInfo.isInitialized = accountInfo.state !== 0;
accountInfo.isFrozen = accountInfo.state === 2;
if (accountInfo.isNativeOption === 1) {
accountInfo.rentExemptReserve = u64.fromBuffer(accountInfo.isNative);
accountInfo.isNative = true;
} else {
accountInfo.rentExemptReserve = null;
accountInfo.isNative = false;
}
if (accountInfo.closeAuthorityOption === 0) {
accountInfo.closeAuthority = null;
} else {
accountInfo.closeAuthority = new PublicKey(accountInfo.closeAuthority);
}
return accountInfo;
};
// TODO: expose in spl package
export const deserializeMint = (data: Buffer) => {
if (data.length !== MintLayout.span) {
throw new Error('Not a valid Mint');
}
const mintInfo = MintLayout.decode(data);
if (mintInfo.mintAuthorityOption === 0) {
mintInfo.mintAuthority = null;
} else {
mintInfo.mintAuthority = new PublicKey(mintInfo.mintAuthority);
}
mintInfo.supply = u64.fromBuffer(mintInfo.supply);
mintInfo.isInitialized = mintInfo.isInitialized !== 0;
if (mintInfo.freezeAuthorityOption === 0) {
mintInfo.freezeAuthority = null;
} else {
mintInfo.freezeAuthority = new PublicKey(mintInfo.freezeAuthority);
}
return mintInfo as MintInfo;
};

View File

@ -0,0 +1,670 @@
import { sleep, useLocalStorageState } from '../utils/utils';
import {
Keypair,
BlockhashAndFeeCalculator,
clusterApiUrl,
Commitment,
Connection,
RpcResponseAndContext,
SignatureStatus,
SimulatedTransactionResponse,
Transaction,
TransactionInstruction,
TransactionSignature,
} from '@solana/web3.js';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { notify } from '../utils/notifications';
import { ExplorerLink } from '../components/ExplorerLink';
import { setProgramIds } from '../utils/ids';
import {
TokenInfo,
TokenListProvider,
ENV as ChainId,
} from '@solana/spl-token-registry';
export type ENV =
| 'mainnet-beta'
| 'mainnet-beta (Solana)'
| 'mainnet-beta (Serum)'
| 'testnet'
| 'devnet'
| 'localnet'
| 'lending';
export const ENDPOINTS = [
{
name: 'mainnet-beta' as ENV,
endpoint: 'https://api.metaplex.solana.com/',
ChainId: ChainId.MainnetBeta,
},
{
name: 'mainnet-beta (Solana)' as ENV,
endpoint: 'https://api.mainnet-beta.solana.com',
ChainId: ChainId.MainnetBeta,
},
{
name: 'mainnet-beta (Serum)' as ENV,
endpoint: 'https://solana-api.projectserum.com/',
ChainId: ChainId.MainnetBeta,
},
{
name: 'testnet' as ENV,
endpoint: clusterApiUrl('testnet'),
ChainId: ChainId.Testnet,
},
{
name: 'devnet' as ENV,
endpoint: clusterApiUrl('devnet'),
ChainId: ChainId.Devnet,
},
];
const DEFAULT = ENDPOINTS[0].endpoint;
const DEFAULT_SLIPPAGE = 0.25;
interface ConnectionConfig {
connection: Connection;
sendConnection: Connection;
endpoint: string;
slippage: number;
setSlippage: (val: number) => void;
env: ENV;
setEndpoint: (val: string) => void;
tokens: TokenInfo[];
tokenMap: Map<string, TokenInfo>;
}
const ConnectionContext = React.createContext<ConnectionConfig>({
endpoint: DEFAULT,
setEndpoint: () => {},
slippage: DEFAULT_SLIPPAGE,
setSlippage: (val: number) => {},
connection: new Connection(DEFAULT, 'recent'),
sendConnection: new Connection(DEFAULT, 'recent'),
env: ENDPOINTS[0].name,
tokens: [],
tokenMap: new Map<string, TokenInfo>(),
});
export function ConnectionProvider({ children = undefined as any }) {
const [endpoint, setEndpoint] = useLocalStorageState(
'connectionEndpoint',
ENDPOINTS[0].endpoint,
);
const [slippage, setSlippage] = useLocalStorageState(
'slippage',
DEFAULT_SLIPPAGE.toString(),
);
const connection = useMemo(
() => new Connection(endpoint, 'recent'),
[endpoint],
);
const sendConnection = useMemo(
() => new Connection(endpoint, 'recent'),
[endpoint],
);
const env =
ENDPOINTS.find(end => end.endpoint === endpoint)?.name || ENDPOINTS[0].name;
const [tokens, setTokens] = useState<TokenInfo[]>([]);
const [tokenMap, setTokenMap] = useState<Map<string, TokenInfo>>(new Map());
useEffect(() => {
// fetch token files
new TokenListProvider().resolve().then(container => {
const list = container
.excludeByTag('nft')
.filterByChainId(
ENDPOINTS.find(end => end.endpoint === endpoint)?.ChainId ||
ChainId.MainnetBeta,
)
.getList();
const knownMints = [...list].reduce((map, item) => {
map.set(item.address, item);
return map;
}, new Map<string, TokenInfo>());
setTokenMap(knownMints);
setTokens(list);
});
}, [env]);
setProgramIds(env);
// The websocket library solana/web3.js uses closes its websocket connection when the subscription list
// is empty after opening its first time, preventing subsequent subscriptions from receiving responses.
// This is a hack to prevent the list from every getting empty
useEffect(() => {
const id = connection.onAccountChange(
Keypair.generate().publicKey,
() => {},
);
return () => {
connection.removeAccountChangeListener(id);
};
}, [connection]);
useEffect(() => {
const id = connection.onSlotChange(() => null);
return () => {
connection.removeSlotChangeListener(id);
};
}, [connection]);
useEffect(() => {
const id = sendConnection.onAccountChange(
Keypair.generate().publicKey,
() => {},
);
return () => {
sendConnection.removeAccountChangeListener(id);
};
}, [sendConnection]);
useEffect(() => {
const id = sendConnection.onSlotChange(() => null);
return () => {
sendConnection.removeSlotChangeListener(id);
};
}, [sendConnection]);
return (
<ConnectionContext.Provider
value={{
endpoint,
setEndpoint,
slippage: parseFloat(slippage),
setSlippage: val => setSlippage(val.toString()),
connection,
sendConnection,
tokens,
tokenMap,
env,
}}
>
{children}
</ConnectionContext.Provider>
);
}
export function useConnection() {
return useContext(ConnectionContext).connection as Connection;
}
export function useSendConnection() {
return useContext(ConnectionContext)?.sendConnection;
}
export function useConnectionConfig() {
const context = useContext(ConnectionContext);
return {
endpoint: context.endpoint,
setEndpoint: context.setEndpoint,
env: context.env,
tokens: context.tokens,
tokenMap: context.tokenMap,
};
}
export function useSlippageConfig() {
const { slippage, setSlippage } = useContext(ConnectionContext);
return { slippage, setSlippage };
}
export const getErrorForTransaction = async (
connection: Connection,
txid: string,
) => {
// wait for all confirmation before geting transaction
await connection.confirmTransaction(txid, 'max');
const tx = await connection.getParsedConfirmedTransaction(txid);
const errors: string[] = [];
if (tx?.meta && tx.meta.logMessages) {
tx.meta.logMessages.forEach(log => {
const regex = /Error: (.*)/gm;
let m;
while ((m = regex.exec(log)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
if (m.length > 1) {
errors.push(m[1]);
}
}
});
}
return errors;
};
export enum SequenceType {
Sequential,
Parallel,
StopOnFailure,
}
export const sendTransactions = async (
connection: Connection,
wallet: any,
instructionSet: TransactionInstruction[][],
signersSet: Keypair[][],
sequenceType: SequenceType = SequenceType.Parallel,
commitment: Commitment = 'singleGossip',
successCallback: (txid: string, ind: number) => void = (txid, ind) => {},
failCallback: (reason: string, ind: number) => boolean = (txid, ind) => false,
block?: BlockhashAndFeeCalculator,
): Promise<number> => {
const unsignedTxns: Transaction[] = [];
if (!block) {
block = await connection.getRecentBlockhash(commitment);
}
for (let i = 0; i < instructionSet.length; i++) {
const instructions = instructionSet[i];
const signers = signersSet[i];
if (instructions.length === 0) {
continue;
}
let transaction = new Transaction();
instructions.forEach(instruction => transaction.add(instruction));
transaction.recentBlockhash = block.blockhash;
transaction.setSigners(
// fee payed by the wallet owner
wallet.publicKey,
...signers.map(s => s.publicKey),
);
if (signers.length > 0) {
transaction.partialSign(...signers);
}
unsignedTxns.push(transaction);
}
const signedTxns = await wallet.signAllTransactions(unsignedTxns);
const pendingTxns: Promise<{ txid: string; slot: number }>[] = [];
let breakEarlyObject = { breakEarly: false, i: 0 };
console.log(
'Signed txns length',
signedTxns.length,
'vs handed in length',
instructionSet.length,
);
for (let i = 0; i < signedTxns.length; i++) {
const signedTxnPromise = sendSignedTransaction({
connection,
signedTransaction: signedTxns[i],
});
signedTxnPromise
.then(({ txid, slot }) => {
successCallback(txid, i);
})
.catch(reason => {
failCallback(signedTxns[i], i);
if (sequenceType === SequenceType.StopOnFailure) {
breakEarlyObject.breakEarly = true;
breakEarlyObject.i = i;
}
});
if (sequenceType !== SequenceType.Parallel) {
try {
await signedTxnPromise;
} catch (e) {
console.log('Caught failure', e);
if (breakEarlyObject.breakEarly) {
console.log('Died on ', breakEarlyObject.i);
return breakEarlyObject.i; // Return the txn we failed on by index
}
}
} else {
pendingTxns.push(signedTxnPromise);
}
}
if (sequenceType !== SequenceType.Parallel) {
await Promise.all(pendingTxns);
}
return signedTxns.length;
};
export const sendTransaction = async (
connection: Connection,
wallet: any,
instructions: TransactionInstruction[],
signers: Keypair[],
awaitConfirmation = true,
commitment: Commitment = 'singleGossip',
includesFeePayer: boolean = false,
block?: BlockhashAndFeeCalculator,
) => {
let transaction = new Transaction();
instructions.forEach(instruction => transaction.add(instruction));
transaction.recentBlockhash = (
block || (await connection.getRecentBlockhash(commitment))
).blockhash;
if (includesFeePayer) {
transaction.setSigners(...signers.map(s => s.publicKey));
} else {
transaction.setSigners(
// fee payed by the wallet owner
wallet.publicKey,
...signers.map(s => s.publicKey),
);
}
if (signers.length > 0) {
transaction.partialSign(...signers);
}
if (!includesFeePayer) {
transaction = await wallet.signTransaction(transaction);
}
const rawTransaction = transaction.serialize();
let options = {
skipPreflight: true,
commitment,
};
const txid = await connection.sendRawTransaction(rawTransaction, options);
let slot = 0;
if (awaitConfirmation) {
const confirmation = await awaitTransactionSignatureConfirmation(
txid,
DEFAULT_TIMEOUT,
connection,
commitment,
);
if (!confirmation)
throw new Error('Timed out awaiting confirmation on transaction');
slot = confirmation?.slot || 0;
if (confirmation?.err) {
const errors = await getErrorForTransaction(connection, txid);
notify({
message: 'Transaction failed...',
description: (
<>
{errors.map(err => (
<div>{err}</div>
))}
<ExplorerLink address={txid} type="transaction" />
</>
),
type: 'error',
});
throw new Error(
`Raw transaction ${txid} failed (${JSON.stringify(status)})`,
);
}
}
return { txid, slot };
};
export const sendTransactionWithRetry = async (
connection: Connection,
wallet: any,
instructions: TransactionInstruction[],
signers: Keypair[],
commitment: Commitment = 'singleGossip',
includesFeePayer: boolean = false,
block?: BlockhashAndFeeCalculator,
beforeSend?: () => void,
) => {
let transaction = new Transaction();
instructions.forEach(instruction => transaction.add(instruction));
transaction.recentBlockhash = (
block || (await connection.getRecentBlockhash(commitment))
).blockhash;
if (includesFeePayer) {
transaction.setSigners(...signers.map(s => s.publicKey));
} else {
transaction.setSigners(
// fee payed by the wallet owner
wallet.publicKey,
...signers.map(s => s.publicKey),
);
}
if (signers.length > 0) {
transaction.partialSign(...signers);
}
if (!includesFeePayer) {
transaction = await wallet.signTransaction(transaction);
}
if (beforeSend) {
beforeSend();
}
const { txid, slot } = await sendSignedTransaction({
connection,
signedTransaction: transaction,
});
return { txid, slot };
};
export const getUnixTs = () => {
return new Date().getTime() / 1000;
};
const DEFAULT_TIMEOUT = 15000;
export async function sendSignedTransaction({
signedTransaction,
connection,
timeout = DEFAULT_TIMEOUT,
}: {
signedTransaction: Transaction;
connection: Connection;
sendingMessage?: string;
sentMessage?: string;
successMessage?: string;
timeout?: number;
}): Promise<{ txid: string; slot: number }> {
const rawTransaction = signedTransaction.serialize();
const startTime = getUnixTs();
let slot = 0;
const txid: TransactionSignature = await connection.sendRawTransaction(
rawTransaction,
{
skipPreflight: true,
},
);
console.log('Started awaiting confirmation for', txid);
let done = false;
(async () => {
while (!done && getUnixTs() - startTime < timeout) {
connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
});
await sleep(500);
}
})();
try {
const confirmation = await awaitTransactionSignatureConfirmation(
txid,
timeout,
connection,
'recent',
true,
);
if (!confirmation)
throw new Error('Timed out awaiting confirmation on transaction');
if (confirmation.err) {
console.error(confirmation.err);
throw new Error('Transaction failed: Custom instruction error');
}
slot = confirmation?.slot || 0;
} catch (err) {
console.error('Timeout Error caught', err);
if (err.timeout) {
throw new Error('Timed out awaiting confirmation on transaction');
}
let simulateResult: SimulatedTransactionResponse | null = null;
try {
simulateResult = (
await simulateTransaction(connection, signedTransaction, 'single')
).value;
} catch (e) {}
if (simulateResult && simulateResult.err) {
if (simulateResult.logs) {
for (let i = simulateResult.logs.length - 1; i >= 0; --i) {
const line = simulateResult.logs[i];
if (line.startsWith('Program log: ')) {
throw new Error(
'Transaction failed: ' + line.slice('Program log: '.length),
);
}
}
}
throw new Error(JSON.stringify(simulateResult.err));
}
// throw new Error('Transaction failed');
} finally {
done = true;
}
console.log('Latency', txid, getUnixTs() - startTime);
return { txid, slot };
}
async function simulateTransaction(
connection: Connection,
transaction: Transaction,
commitment: Commitment,
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
// @ts-ignore
transaction.recentBlockhash = await connection._recentBlockhash(
// @ts-ignore
connection._disableBlockhashCaching,
);
const signData = transaction.serializeMessage();
// @ts-ignore
const wireTransaction = transaction._serialize(signData);
const encodedTransaction = wireTransaction.toString('base64');
const config: any = { encoding: 'base64', commitment };
const args = [encodedTransaction, config];
// @ts-ignore
const res = await connection._rpcRequest('simulateTransaction', args);
if (res.error) {
throw new Error('failed to simulate transaction: ' + res.error.message);
}
return res.result;
}
async function awaitTransactionSignatureConfirmation(
txid: TransactionSignature,
timeout: number,
connection: Connection,
commitment: Commitment = 'recent',
queryStatus = false,
): Promise<SignatureStatus | null | void> {
let done = false;
let status: SignatureStatus | null | void = {
slot: 0,
confirmations: 0,
err: null,
};
let subId = 0;
status = await new Promise(async (resolve, reject) => {
setTimeout(() => {
if (done) {
return;
}
done = true;
console.log('Rejecting for timeout...');
reject({ timeout: true });
}, timeout);
try {
subId = connection.onSignature(
txid,
(result, context) => {
done = true;
status = {
err: result.err,
slot: context.slot,
confirmations: 0,
};
if (result.err) {
console.log('Rejected via websocket', result.err);
reject(status);
} else {
console.log('Resolved via websocket', result);
resolve(status);
}
},
commitment,
);
} catch (e) {
done = true;
console.error('WS error in setup', txid, e);
}
while (!done && queryStatus) {
// eslint-disable-next-line no-loop-func
(async () => {
try {
const signatureStatuses = await connection.getSignatureStatuses([
txid,
]);
status = signatureStatuses && signatureStatuses.value[0];
if (!done) {
if (!status) {
console.log('REST null result for', txid, status);
} else if (status.err) {
console.log('REST error for', txid, status);
done = true;
reject(status.err);
} else if (!status.confirmations) {
console.log('REST no confirmations for', txid, status);
} else {
console.log('REST confirmation for', txid, status);
done = true;
resolve(status);
}
}
} catch (e) {
if (!done) {
console.log('REST connection error: txid', txid, e);
}
}
})();
await sleep(2000);
}
});
//@ts-ignore
if (connection._signatureSubscriptions[subId])
connection.removeSignatureListener(subId);
done = true;
console.log('Returning status', status);
return status;
}

View File

@ -0,0 +1,8 @@
export * as Accounts from './accounts';
export * as Connection from './connection';
export * as Wallet from './wallet';
export { ParsedAccount, ParsedAccountBase } from './accounts';
export * from './accounts';
export * from './wallet';
export * from './connection';

View File

@ -0,0 +1,6 @@
p {
color: #6d6d6d;
font-family: Inter, sans-serif;
font-style: normal;
font-size: 0.9rem;
}

View File

@ -0,0 +1,236 @@
import { WalletAdapter } from "@solana/wallet-base";
import Wallet from "@project-serum/sol-wallet-adapter";
import { Button } from "antd";
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { notify } from "./../utils/notifications";
import { useConnectionConfig } from "./connection";
import { useLocalStorageState } from "../utils/utils";
import { PhantomWalletAdapter } from "../wallet-adapters/phantom";
import { useLocation } from "react-router";
import { MetaplexModal } from "../components/MetaplexModal";
import './wallet.css'
const ASSETS_URL = 'https://raw.githubusercontent.com/solana-labs/oyster/main/assets/wallets/';
export const WALLET_PROVIDERS = [
{
name: "Phantom",
url: "https://www.phantom.app",
icon: `https://www.phantom.app/img/logo.png`,
adapter: PhantomWalletAdapter,
},
{
name: "Sollet",
url: "https://www.sollet.io",
icon: `${ASSETS_URL}sollet.svg`,
},
{
name: "MathWallet",
url: "https://mathwallet.org",
icon: `${ASSETS_URL}mathwallet.svg`,
},
// {
// name: 'Torus',
// url: 'https://tor.us',
// icon: `${ASSETS_URL}torus.svg`,
// adapter: TorusWalletAdapter,
// }
];
const WalletContext = React.createContext<{
wallet: WalletAdapter | undefined,
connected: boolean,
select: () => void,
provider: typeof WALLET_PROVIDERS[number] | undefined,
}>({
wallet: undefined,
connected: false,
select() { },
provider: undefined,
});
export function WalletProvider({ children = null as any }) {
const { endpoint } = useConnectionConfig();
const location = useLocation();
const [autoConnect, setAutoConnect] = useState(location.pathname.indexOf('result=') >= 0 || false);
const [providerUrl, setProviderUrl] = useLocalStorageState("walletProvider");
const provider = useMemo(() => WALLET_PROVIDERS.find(({ url }) => url === providerUrl), [providerUrl]);
const wallet = useMemo(function () {
if (provider) {
return new (provider.adapter || Wallet)(providerUrl, endpoint) as WalletAdapter;
}
}, [provider, providerUrl, endpoint]);
const [connected, setConnected] = useState(false);
useEffect(() => {
if (wallet?.publicKey && connected) {
const walletPublicKey = wallet.publicKey.toBase58();
const keyToDisplay =
walletPublicKey.length > 20
? `${walletPublicKey.substring(0, 7)}.....${walletPublicKey.substring(
walletPublicKey.length - 7,
walletPublicKey.length
)}`
: walletPublicKey;
notify({
message: "Wallet update",
description: "Connected to wallet " + keyToDisplay,
});
}
}, [connected])
useEffect(() => {
if (wallet) {
wallet.on("connect", () => {
if (wallet.publicKey) {
setConnected(true);
}
});
wallet.on("disconnect", () => {
setConnected(false);
notify({
message: "Wallet update",
description: "Disconnected from wallet",
});
});
}
return () => {
setConnected(false);
if (wallet) {
wallet.disconnect();
}
};
}, [wallet]);
useEffect(() => {
if (wallet && autoConnect) {
wallet.connect();
setAutoConnect(false);
}
return () => { }
}, [wallet, autoConnect]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [showProviders, setShowProviders] = useState(false);
const select = useCallback(() => setIsModalVisible(true), []);
const close = useCallback(() => {
setIsModalVisible(false)
setShowProviders(false)
}, []);
const pp = WALLET_PROVIDERS.find(wp => wp.name === "Phantom")
return (
<WalletContext.Provider
value={{
wallet,
connected,
select,
provider,
}}
>
{children}
<MetaplexModal
visible={isModalVisible}
onCancel={close}
>
<div style={{
background: 'linear-gradient(180deg, #D329FC 0%, #8F6DDE 49.48%, #19E6AD 100%)',
borderRadius: 36,
width: 50,
height: 50,
textAlign: 'center',
verticalAlign: 'middle',
fontWeight: 700,
fontSize: '1.3rem',
lineHeight: 2.4,
marginBottom: 10,
}}>M</div>
<h2>{provider ? 'Change provider' : 'Welcome to Metaplex'}</h2>
<p>{provider ? 'Feel free to switch wallet provider' : 'You must be signed in to place a bid'}</p>
<br />
{(provider || showProviders) ? <>
{WALLET_PROVIDERS.map((provider, idx) => {
if (providerUrl === provider.url) return null
const onClick = function () {
setProviderUrl(provider.url);
setAutoConnect(true);
close();
}
return (
<Button
key={idx}
size="large"
type={providerUrl === provider.url ? "primary" : "ghost"}
onClick={onClick}
icon={
<img
alt={`${provider.name}`}
width={20}
height={20}
src={provider.icon}
style={{ marginRight: 8 }} />
}
style={{
display: "block",
width: "100%",
textAlign: "left",
marginBottom: 8,
}}>{provider.name}</Button>
)
})}
</> : <>
<Button
className="metaplex-button"
style={{
width: '80%',
fontWeight: 'unset',
}}
onClick={_ => {
setProviderUrl(pp?.url);
setAutoConnect(true);
close();
}}
>
<span><img src={pp?.icon} style={{ width: '1.2rem' }} />&nbsp;Sign in with Phantom</span>
<span>&gt;</span>
</Button>
<p onClick={_ => setShowProviders(true)} style={{ cursor: 'pointer', marginTop: 10 }}>
Select a different Solana wallet
</p>
</>}
</MetaplexModal>
</WalletContext.Provider>
);
}
export const useWallet = () => {
const { wallet, connected, provider, select } = useContext(WalletContext);
return {
wallet,
connected,
provider,
select,
connect() {
wallet ? wallet.connect() : select();
},
disconnect() {
wallet?.disconnect();
},
};
}

View File

@ -0,0 +1,120 @@
import {
MintLayout,
AccountLayout,
Token,
AuthorityType,
} from '@solana/spl-token';
import {
Connection,
PublicKey,
Transaction,
Account,
SystemProgram,
} from '@solana/web3.js';
export const mintNFT = async (
connection: Connection,
wallet: {
publicKey: PublicKey;
signTransaction: (tx: Transaction) => Transaction;
},
// SOL account
owner: PublicKey,
) => {
const TOKEN_PROGRAM_ID = new PublicKey(
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
);
const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new PublicKey(
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL',
);
const mintAccount = new Account();
const tokenAccount = new Account();
// Allocate memory for the account
const mintRent = await connection.getMinimumBalanceForRentExemption(
MintLayout.span,
);
const accountRent = await connection.getMinimumBalanceForRentExemption(
MintLayout.span,
);
let transaction = new Transaction();
const signers = [mintAccount, tokenAccount];
transaction.recentBlockhash = (
await connection.getRecentBlockhash('max')
).blockhash;
transaction.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: mintAccount.publicKey,
lamports: mintRent,
space: MintLayout.span,
programId: TOKEN_PROGRAM_ID,
}),
);
transaction.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: tokenAccount.publicKey,
lamports: accountRent,
space: AccountLayout.span,
programId: TOKEN_PROGRAM_ID,
}),
);
transaction.add(
Token.createInitMintInstruction(
TOKEN_PROGRAM_ID,
mintAccount.publicKey,
0,
wallet.publicKey,
wallet.publicKey,
),
);
transaction.add(
Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
mintAccount.publicKey,
tokenAccount.publicKey,
owner,
),
);
transaction.add(
Token.createMintToInstruction(
TOKEN_PROGRAM_ID,
mintAccount.publicKey,
tokenAccount.publicKey,
wallet.publicKey,
[],
1,
),
);
transaction.add(
Token.createSetAuthorityInstruction(
TOKEN_PROGRAM_ID,
mintAccount.publicKey,
null,
'MintTokens',
wallet.publicKey,
[],
),
);
transaction.setSigners(wallet.publicKey, ...signers.map(s => s.publicKey));
if (signers.length > 0) {
transaction.partialSign(...signers);
}
transaction = await wallet.signTransaction(transaction);
const rawTransaction = transaction.serialize();
let options = {
skipPreflight: true,
commitment: 'singleGossip',
};
const txid = await connection.sendRawTransaction(rawTransaction, options);
return { txid, mint: mintAccount.publicKey, account: tokenAccount.publicKey };
};

View File

@ -0,0 +1,4 @@
export * from './useUserAccounts';
export * from './useAccountByMint';
export * from './useTokenName';
export * from './useThatState';

View File

@ -0,0 +1,17 @@
import { PublicKey } from '@solana/web3.js';
import { useUserAccounts } from '../hooks/useUserAccounts';
export const useAccountByMint = (mint?: string | PublicKey) => {
const { userAccounts } = useUserAccounts();
const mintAddress = typeof mint === 'string' ? mint : mint?.toBase58();
const index = userAccounts.findIndex(
acc => acc.info.mint.toBase58() === mintAddress,
);
if (index !== -1) {
return userAccounts[index];
}
return;
};

View File

@ -0,0 +1,16 @@
import { useState } from 'react';
// Extends useState() hook with async getThatState getter which can be used to get state value in contexts (ex. async callbacks) where up to date state is not available
export function useThatState<T>(initialState: T) {
const [state, setState] = useState<T>(initialState);
const getThatState = () =>
new Promise<T>(resolve => {
// Use NOP setState call to retrieve current state value
setState(s => {
resolve(s);
return s;
});
});
return [state, setState, getThatState] as const;
}

View File

@ -0,0 +1,10 @@
import { PublicKey } from '@solana/web3.js';
import { useConnectionConfig } from '../contexts/connection';
import { getTokenName } from '../utils/utils';
export function useTokenName(mintAddress?: string | PublicKey) {
const { tokenMap } = useConnectionConfig();
const address =
typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58();
return getTokenName(tokenMap, address);
}

View File

@ -0,0 +1,21 @@
import { TokenAccount } from '../models';
import { useAccountsContext } from '../contexts/accounts';
export function useUserAccounts(): {
userAccounts: TokenAccount[];
accountByMint: Map<string, TokenAccount>;
} {
const context = useAccountsContext();
const accountByMint = context.userAccounts.reduce(
(prev: Map<string, TokenAccount>, acc: TokenAccount) => {
prev.set(acc.info.mint.toBase58(), acc);
return prev;
},
new Map<string, TokenAccount>(),
);
return {
userAccounts: context.userAccounts as TokenAccount[],
accountByMint,
};
}

View File

@ -0,0 +1,17 @@
export * as actions from './actions';
export * from './actions';
export * as components from './components';
export * from './components'; // Allow direct exports too
export * as constants from './constants';
export * as hooks from './hooks';
export * from './hooks';
export * as contexts from './contexts';
export * from './contexts';
export * as models from './models';
export * as utils from './utils';
export * from './utils';
export * as walletAdapters from './wallet-adapters';
export { TokenAccount } from './models';
export { ParsedAccount, ParsedAccountBase } from './contexts';
export { KnownTokenMap, EventEmitter, Layout } from './utils';

View File

@ -0,0 +1,85 @@
import {
AccountInfo,
Keypair,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import { AccountInfo as TokenAccountInfo, Token } from '@solana/spl-token';
import { TOKEN_PROGRAM_ID } from '../utils/ids';
import BufferLayout from 'buffer-layout';
export interface TokenAccount {
pubkey: PublicKey;
account: AccountInfo<Buffer>;
info: TokenAccountInfo;
}
export interface ParsedDataAccount {
amount: number;
rawAmount: string;
parsedAssetAddress: string;
parsedAccount: any;
assetDecimals: number;
assetIcon: any;
name: string;
symbol: string;
sourceAddress: string;
targetAddress: string;
}
export const ParsedDataLayout = BufferLayout.struct([
BufferLayout.blob(32, 'amount'),
BufferLayout.u8('toChain'),
BufferLayout.blob(32, 'sourceAddress'),
BufferLayout.blob(32, 'targetAddress'),
BufferLayout.blob(32, 'assetAddress'),
BufferLayout.u8('assetChain'),
BufferLayout.u8('assetDecimals'),
BufferLayout.seq(BufferLayout.u8(), 1), // 4 byte alignment because a u32 is following
BufferLayout.u32('nonce'),
BufferLayout.blob(1001, 'vaa'),
BufferLayout.seq(BufferLayout.u8(), 3), // 4 byte alignment because a u32 is following
BufferLayout.u32('vaaTime'),
BufferLayout.u32('lockupTime'),
BufferLayout.u8('pokeCounter'),
BufferLayout.blob(32, 'signatureAccount'),
BufferLayout.u8('initialized'),
]);
export function approve(
instructions: TransactionInstruction[],
cleanupInstructions: TransactionInstruction[],
account: PublicKey,
owner: PublicKey,
amount: number,
autoRevoke = true,
// if delegate is not passed ephemeral transfer authority is used
delegate?: PublicKey,
existingTransferAuthority?: Keypair,
): Keypair {
const tokenProgram = TOKEN_PROGRAM_ID;
const transferAuthority = existingTransferAuthority || Keypair.generate();
const delegateKey = delegate ?? transferAuthority.publicKey;
instructions.push(
Token.createApproveInstruction(
tokenProgram,
account,
delegate ?? transferAuthority.publicKey,
owner,
[],
amount,
),
);
if (autoRevoke) {
cleanupInstructions.push(
Token.createRevokeInstruction(tokenProgram, account, owner, []),
);
}
return transferAuthority;
}

View File

@ -0,0 +1,2 @@
export * from './account';
export * from './tokenSwap';

View File

@ -0,0 +1,82 @@
import * as BufferLayout from 'buffer-layout';
import { publicKey, uint64 } from '../utils/layout';
export { TokenSwap } from '@solana/spl-token-swap';
const FEE_LAYOUT = BufferLayout.struct(
[
BufferLayout.nu64('tradeFeeNumerator'),
BufferLayout.nu64('tradeFeeDenominator'),
BufferLayout.nu64('ownerTradeFeeNumerator'),
BufferLayout.nu64('ownerTradeFeeDenominator'),
BufferLayout.nu64('ownerWithdrawFeeNumerator'),
BufferLayout.nu64('ownerWithdrawFeeDenominator'),
BufferLayout.nu64('hostFeeNumerator'),
BufferLayout.nu64('hostFeeDenominator'),
],
'fees',
);
export const TokenSwapLayoutLegacyV0 = BufferLayout.struct([
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
publicKey('tokenAccountA'),
publicKey('tokenAccountB'),
publicKey('tokenPool'),
uint64('feesNumerator'),
uint64('feesDenominator'),
]);
export const TokenSwapLayoutV1: typeof BufferLayout.Structure =
BufferLayout.struct([
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
publicKey('tokenProgramId'),
publicKey('tokenAccountA'),
publicKey('tokenAccountB'),
publicKey('tokenPool'),
publicKey('mintA'),
publicKey('mintB'),
publicKey('feeAccount'),
BufferLayout.u8('curveType'),
uint64('tradeFeeNumerator'),
uint64('tradeFeeDenominator'),
uint64('ownerTradeFeeNumerator'),
uint64('ownerTradeFeeDenominator'),
uint64('ownerWithdrawFeeNumerator'),
uint64('ownerWithdrawFeeDenominator'),
BufferLayout.blob(16, 'padding'),
]);
const CURVE_NODE = BufferLayout.union(
BufferLayout.u8(),
BufferLayout.blob(32),
'curve',
);
CURVE_NODE.addVariant(0, BufferLayout.struct([]), 'constantProduct');
CURVE_NODE.addVariant(
1,
BufferLayout.struct([BufferLayout.nu64('token_b_price')]),
'constantPrice',
);
CURVE_NODE.addVariant(2, BufferLayout.struct([]), 'stable');
CURVE_NODE.addVariant(
3,
BufferLayout.struct([BufferLayout.nu64('token_b_offset')]),
'offset',
);
export const TokenSwapLayout: typeof BufferLayout.Structure =
BufferLayout.struct([
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
publicKey('tokenProgramId'),
publicKey('tokenAccountA'),
publicKey('tokenAccountB'),
publicKey('tokenPool'),
publicKey('mintA'),
publicKey('mintB'),
publicKey('feeAccount'),
FEE_LAYOUT,
CURVE_NODE,
]);

View File

@ -0,0 +1,9 @@
declare module 'buffer-layout' {
const bl: any;
export = bl;
}
declare module 'jazzicon' {
const jazzicon: any;
export = jazzicon;
}

View File

@ -0,0 +1,4 @@
declare module '@project-serum/sol-wallet-adapter' {
const adapter: any;
export = adapter;
}

6
js/packages/common/src/types/u64.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import BN from 'bn.js';
export class u64 extends BN {
toBuffer(): Buffer;
static fromBuffer(buffer: Buffer): u64;
}

View File

@ -0,0 +1,17 @@
import { PublicKey } from '@solana/web3.js';
import { BinaryReader, BinaryWriter } from 'borsh';
export const extendBorsh = () => {
(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());
};
};
extendBorsh();

View File

@ -0,0 +1,60 @@
import { EventEmitter as Emitter } from 'eventemitter3';
export class CacheUpdateEvent {
static type = 'CacheUpdate';
id: string;
parser: any;
isNew: boolean;
constructor(id: string, isNew: boolean, parser: any) {
this.id = id;
this.parser = parser;
this.isNew = isNew;
}
}
export class CacheDeleteEvent {
static type = 'CacheUpdate';
id: string;
constructor(id: string) {
this.id = id;
}
}
export class MarketUpdateEvent {
static type = 'MarketUpdate';
ids: Set<string>;
constructor(ids: Set<string>) {
this.ids = ids;
}
}
export class EventEmitter {
private emitter = new Emitter();
onMarket(callback: (args: MarketUpdateEvent) => void) {
this.emitter.on(MarketUpdateEvent.type, callback);
return () => this.emitter.removeListener(MarketUpdateEvent.type, callback);
}
onCache(callback: (args: CacheUpdateEvent) => void) {
this.emitter.on(CacheUpdateEvent.type, callback);
return () => this.emitter.removeListener(CacheUpdateEvent.type, callback);
}
raiseMarketUpdated(ids: Set<string>) {
this.emitter.emit(MarketUpdateEvent.type, new MarketUpdateEvent(ids));
}
raiseCacheUpdated(id: string, isNew: boolean, parser: any) {
this.emitter.emit(
CacheUpdateEvent.type,
new CacheUpdateEvent(id, isNew, parser),
);
}
raiseCacheDeleted(id: string) {
this.emitter.emit(CacheDeleteEvent.type, new CacheDeleteEvent(id));
}
}

View File

@ -0,0 +1,192 @@
import { PublicKey } from '@solana/web3.js';
import { TokenSwapLayout, TokenSwapLayoutV1 } from '../models/tokenSwap';
export const STORE = new PublicKey(
'3Eu2LaWpSGcJYiMEFaUw5DvD2uuzxuRmSp9sUZLhLTAF',
);
export const WRAPPED_SOL_MINT = new PublicKey(
'So11111111111111111111111111111111111111112',
);
export let TOKEN_PROGRAM_ID = new PublicKey(
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
);
export let LENDING_PROGRAM_ID = new PublicKey(
'LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi',
);
export let SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new PublicKey(
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL',
);
export let BPF_UPGRADE_LOADER_ID = new PublicKey(
'BPFLoaderUpgradeab1e11111111111111111111111',
);
export const METADATA_PROGRAM_ID = new PublicKey(
'metaeyJokjzVwvcuDFX3rWAKvPYgGGqbGxXbcufPhBY',
);
export const MEMO_ID = new PublicKey(
'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr',
);
export const VAULT_ID = new PublicKey(
'vau1pEhWRVvv148kMr4yaGG3x6ATgjqsk3MyPG33vsR',
);
export const AUCTION_ID = new PublicKey(
'auctZEoymtKdckLLiq3pz5BmdWsXu4Aq3aLKgtT6zAt',
);
export const METAPLEX_ID = new PublicKey(
'p1exrXi4GNg282QUXAEznqpxRd2DUSHewz586MzHP8K',
);
export let SYSTEM = new PublicKey('11111111111111111111111111111111');
let WORMHOLE_BRIDGE: {
pubkey: PublicKey;
bridge: string;
wrappedMaster: string;
};
let GOVERNANCE: {
programId: PublicKey;
};
let SWAP_PROGRAM_ID: PublicKey;
let SWAP_PROGRAM_LEGACY_IDS: PublicKey[];
let SWAP_PROGRAM_LAYOUT: any;
export const LEND_HOST_FEE_ADDRESS = process.env.REACT_APP_LEND_HOST_FEE_ADDRESS
? new PublicKey(`${process.env.REACT_APP_LEND_HOST_FEE_ADDRESS}`)
: undefined;
console.debug(`Lend host fee address: ${LEND_HOST_FEE_ADDRESS?.toBase58()}`);
export const ENABLE_FEES_INPUT = false;
// legacy pools are used to show users contributions in those pools to allow for withdrawals of funds
export const PROGRAM_IDS = [
{
name: 'mainnet-beta',
governance: () => ({
programId: new PublicKey('9iAeqqppjn7g1Jn8o2cQCqU5aQVV3h4q9bbWdKRbeC2w'),
}),
wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
bridge: '0xf92cD566Ea4864356C5491c177A430C222d7e678',
wrappedMaster: '9A5e27995309a03f8B583feBdE7eF289FcCdC6Ae',
}),
swap: () => ({
current: {
pubkey: new PublicKey('9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL'),
layout: TokenSwapLayoutV1,
},
legacy: [
// TODO: uncomment to enable legacy contract
// new PublicKey("9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL"),
],
}),
},
{
name: 'testnet',
governance: () => ({
programId: new PublicKey('A9KW1nhwZUr1kMX8C6rgzZvAE9AwEEUi2C77SiVvEiuN'),
}),
wormhole: () => ({
pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'),
bridge: '0x251bBCD91E84098509beaeAfF0B9951859af66D3',
wrappedMaster: 'E39f0b145C0aF079B214c5a8840B2B01eA14794c',
}),
swap: () => ({
current: {
pubkey: new PublicKey('2n2dsFSgmPcZ8jkmBZLGUM2nzuFqcBGQ3JEEj6RJJcEg'),
layout: TokenSwapLayoutV1,
},
legacy: [],
}),
},
{
name: 'devnet',
governance: () => ({
programId: new PublicKey('A9KW1nhwZUr1kMX8C6rgzZvAE9AwEEUi2C77SiVvEiuN'),
}),
wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
bridge: '0xf92cD566Ea4864356C5491c177A430C222d7e678',
wrappedMaster: '9A5e27995309a03f8B583feBdE7eF289FcCdC6Ae',
}),
swap: () => ({
current: {
pubkey: new PublicKey('6Cust2JhvweKLh4CVo1dt21s2PJ86uNGkziudpkNPaCj'),
layout: TokenSwapLayout,
},
legacy: [new PublicKey('BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ')],
}),
},
{
name: 'localnet',
governance: () => ({
programId: new PublicKey('2uWrXQ3tMurqTLe3Dmue6DzasUGV9UPqK7AK7HzS7v3D'),
}),
wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
bridge: '0xf92cD566Ea4864356C5491c177A430C222d7e678',
wrappedMaster: '9A5e27995309a03f8B583feBdE7eF289FcCdC6Ae',
}),
swap: () => ({
current: {
pubkey: new PublicKey('369YmCWHGxznT7GGBhcLZDRcRoGWmGKFWdmtiPy78yj7'),
layout: TokenSwapLayoutV1,
},
legacy: [],
}),
},
];
export const setProgramIds = (envName: string) => {
let instance = PROGRAM_IDS.find(env => envName.indexOf(env.name) >= 0);
if (!instance) {
return;
}
WORMHOLE_BRIDGE = instance.wormhole();
let swap = instance.swap();
SWAP_PROGRAM_ID = swap.current.pubkey;
SWAP_PROGRAM_LAYOUT = swap.current.layout;
SWAP_PROGRAM_LEGACY_IDS = swap.legacy;
GOVERNANCE = instance.governance();
if (envName === 'mainnet-beta') {
LENDING_PROGRAM_ID = new PublicKey(
'LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi',
);
}
};
export const programIds = () => {
return {
token: TOKEN_PROGRAM_ID,
swap: SWAP_PROGRAM_ID,
swap_legacy: SWAP_PROGRAM_LEGACY_IDS,
swapLayout: SWAP_PROGRAM_LAYOUT,
lending: LENDING_PROGRAM_ID,
wormhole: WORMHOLE_BRIDGE,
governance: GOVERNANCE,
associatedToken: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
bpf_upgrade_loader: BPF_UPGRADE_LOADER_ID,
system: SYSTEM,
metadata: METADATA_PROGRAM_ID,
memo: MEMO_ID,
vault: VAULT_ID,
auction: AUCTION_ID,
metaplex: METAPLEX_ID,
store: STORE,
};
};

View File

@ -0,0 +1,8 @@
export * from './eventEmitter';
export * from './ids';
export * as Layout from './layout';
export * from './notifications';
export * from './utils';
export * from './strings';
export * as shortvec from './shortvec';
export * from './borsh';

View File

@ -0,0 +1,121 @@
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
/**
* Layout for a public key
*/
export const publicKey = (property = 'publicKey'): unknown => {
const publicKeyLayout = BufferLayout.blob(32, property);
const _decode = publicKeyLayout.decode.bind(publicKeyLayout);
const _encode = publicKeyLayout.encode.bind(publicKeyLayout);
publicKeyLayout.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset);
return new PublicKey(data);
};
publicKeyLayout.encode = (key: PublicKey, buffer: Buffer, offset: number) => {
return _encode(key.toBuffer(), buffer, offset);
};
return publicKeyLayout;
};
/**
* Layout for a 64bit unsigned value
*/
export const uint64 = (property = 'uint64'): unknown => {
const layout = BufferLayout.blob(8, property);
const _decode = layout.decode.bind(layout);
const _encode = layout.encode.bind(layout);
layout.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset);
return new BN(
[...data]
.reverse()
.map(i => `00${i.toString(16)}`.slice(-2))
.join(''),
16,
);
};
layout.encode = (num: BN, buffer: Buffer, offset: number) => {
const a = num.toArray().reverse();
let b = Buffer.from(a);
if (b.length !== 8) {
const zeroPad = Buffer.alloc(8);
b.copy(zeroPad);
b = zeroPad;
}
return _encode(b, buffer, offset);
};
return layout;
};
// TODO: wrap in BN (what about decimals?)
export const uint128 = (property = 'uint128'): unknown => {
const layout = BufferLayout.blob(16, property);
const _decode = layout.decode.bind(layout);
const _encode = layout.encode.bind(layout);
layout.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset);
return new BN(
[...data]
.reverse()
.map(i => `00${i.toString(16)}`.slice(-2))
.join(''),
16,
);
};
layout.encode = (num: BN, buffer: Buffer, offset: number) => {
const a = num.toArray().reverse();
let b = Buffer.from(a);
if (b.length !== 16) {
const zeroPad = Buffer.alloc(16);
b.copy(zeroPad);
b = zeroPad;
}
return _encode(b, buffer, offset);
};
return layout;
};
/**
* Layout for a Rust String type
*/
export const rustString = (property = 'string'): unknown => {
const rsl = BufferLayout.struct(
[
BufferLayout.u32('length'),
BufferLayout.u32('lengthPadding'),
BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), 'chars'),
],
property,
);
const _decode = rsl.decode.bind(rsl);
const _encode = rsl.encode.bind(rsl);
rsl.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset);
return data.chars.toString('utf8');
};
rsl.encode = (str: string, buffer: Buffer, offset: number) => {
const data = {
chars: Buffer.from(str, 'utf8'),
};
return _encode(data, buffer, offset);
};
return rsl;
};

View File

@ -0,0 +1,33 @@
import React from "react";
import { notification } from "antd";
// import Link from '../components/Link';
export function notify({
message = "",
description = undefined as any,
txid = "",
type = "info",
placement = "bottomLeft",
}) {
if (txid) {
// <Link
// external
// to={'https://explorer.solana.com/tx/' + txid}
// style={{ color: '#0000ff' }}
// >
// View transaction {txid.slice(0, 8)}...{txid.slice(txid.length - 8)}
// </Link>
description = <></>;
}
(notification as any)[type]({
message: <span style={{ color: "black" }}>{message}</span>,
description: (
<span style={{ color: "black", opacity: 0.5 }}>{description}</span>
),
placement,
style: {
backgroundColor: "white",
},
});
}

View File

@ -0,0 +1,30 @@
export function decodeLength(bytes: Array<number>): number {
let len = 0;
let size = 0;
for (;;) {
let elem = bytes.shift();
//@ts-ignore
len |= (elem & 0x7f) << (size * 7);
size += 1;
//@ts-ignore
if ((elem & 0x80) === 0) {
break;
}
}
return len;
}
export function encodeLength(bytes: Array<number>, len: number) {
let rem_len = len;
for (;;) {
let elem = rem_len & 0x7f;
rem_len >>= 7;
if (rem_len === 0) {
bytes.push(elem);
break;
} else {
elem |= 0x80;
bytes.push(elem);
}
}
}

View File

@ -0,0 +1,74 @@
// credit https://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array
export function toUTF8Array(str: string) {
let utf8 = [];
for (let i = 0; i < str.length; i++) {
let charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
} else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(
0xe0 | (charcode >> 12),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f),
);
}
// surrogate pair
else {
i++;
// UTF-16 encodes 0x10000-0x10FFFF by
// subtracting 0x10000 and splitting the
// 20 bits of 0x0-0xFFFFF into two halves
charcode =
0x10000 + (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
utf8.push(
0xf0 | (charcode >> 18),
0x80 | ((charcode >> 12) & 0x3f),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f),
);
}
}
return utf8;
}
//courtesy https://gist.github.com/joni/3760795
export function fromUTF8Array(data: number[]) {
// array of bytes
let str = '',
i;
for (i = 0; i < data.length; i++) {
const value = data[i];
if (value < 0x80) {
str += String.fromCharCode(value);
} else if (value > 0xbf && value < 0xe0) {
str += String.fromCharCode(((value & 0x1f) << 6) | (data[i + 1] & 0x3f));
i += 1;
} else if (value > 0xdf && value < 0xf0) {
str += String.fromCharCode(
((value & 0x0f) << 12) |
((data[i + 1] & 0x3f) << 6) |
(data[i + 2] & 0x3f),
);
i += 2;
} else {
// surrogate pair
const charCode =
(((value & 0x07) << 18) |
((data[i + 1] & 0x3f) << 12) |
((data[i + 2] & 0x3f) << 6) |
(data[i + 3] & 0x3f)) -
0x010000;
str += String.fromCharCode(
(charCode >> 10) | 0xd800,
(charCode & 0x03ff) | 0xdc00,
);
i += 3;
}
}
return str;
}

View File

@ -0,0 +1,261 @@
import { useCallback, useState } from 'react';
import { MintInfo } from '@solana/spl-token';
import { TokenAccount } from './../models';
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { WAD, ZERO } from '../constants';
import { TokenInfo } from '@solana/spl-token-registry';
export type KnownTokenMap = Map<string, TokenInfo>;
export const formatPriceNumber = new Intl.NumberFormat('en-US', {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 8,
});
export function useLocalStorageState(key: string, defaultState?: string) {
const [state, setState] = useState(() => {
// NOTE: Not sure if this is ok
const storedState = localStorage.getItem(key);
if (storedState) {
return JSON.parse(storedState);
}
return defaultState;
});
const setLocalStorageState = useCallback(
newState => {
const changed = state !== newState;
if (!changed) {
return;
}
setState(newState);
if (newState === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(newState));
}
},
[state, key],
);
return [state, setLocalStorageState];
}
// shorten the checksummed version of the input address to have 4 characters at start and end
export function shortenAddress(address: string, chars = 4): string {
return `${address.slice(0, chars)}...${address.slice(-chars)}`;
}
export function getTokenName(
map: KnownTokenMap,
mint?: string | PublicKey,
shorten = true,
): string {
const mintAddress = typeof mint === 'string' ? mint : mint?.toBase58();
if (!mintAddress) {
return 'N/A';
}
const knownSymbol = map.get(mintAddress)?.symbol;
if (knownSymbol) {
return knownSymbol;
}
return shorten ? `${mintAddress.substring(0, 5)}...` : mintAddress;
}
export function getVerboseTokenName(
map: KnownTokenMap,
mint?: string | PublicKey,
shorten = true,
): string {
const mintAddress = typeof mint === 'string' ? mint : mint?.toBase58();
if (!mintAddress) {
return 'N/A';
}
const knownName = map.get(mintAddress)?.name;
if (knownName) {
return knownName;
}
return shorten ? `${mintAddress.substring(0, 5)}...` : mintAddress;
}
export function getTokenByName(tokenMap: KnownTokenMap, name: string) {
let token: TokenInfo | null = null;
for (const val of tokenMap.values()) {
if (val.symbol === name) {
token = val;
break;
}
}
return token;
}
export function getTokenIcon(
map: KnownTokenMap,
mintAddress?: string | PublicKey,
): string | undefined {
const address =
typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58();
if (!address) {
return;
}
return map.get(address)?.logoURI;
}
export function isKnownMint(map: KnownTokenMap, mintAddress: string) {
return !!map.get(mintAddress);
}
export const STABLE_COINS = new Set(['USDC', 'wUSDC', 'USDT']);
export function chunks<T>(array: T[], size: number): T[][] {
return Array.apply<number, T[], T[][]>(
0,
new Array(Math.ceil(array.length / size)),
).map((_, index) => array.slice(index * size, (index + 1) * size));
}
export function toLamports(
account?: TokenAccount | number,
mint?: MintInfo,
): number {
if (!account) {
return 0;
}
const amount =
typeof account === 'number' ? account : account.info.amount?.toNumber();
const precision = Math.pow(10, mint?.decimals || 0);
return Math.floor(amount * precision);
}
export function wadToLamports(amount?: BN): BN {
return amount?.div(WAD) || ZERO;
}
export function fromLamports(
account?: TokenAccount | number | BN,
mint?: MintInfo,
rate: number = 1.0,
): number {
if (!account) {
return 0;
}
const amount = Math.floor(
typeof account === 'number'
? account
: BN.isBN(account)
? account.toNumber()
: account.info.amount.toNumber(),
);
const precision = Math.pow(10, mint?.decimals || 9);
return (amount / precision) * rate;
}
export const tryParseKey = (key: string): PublicKey | null => {
try {
return new PublicKey(key);
} catch (error) {
return null;
}
};
var SI_SYMBOL = ['', 'k', 'M', 'G', 'T', 'P', 'E'];
const abbreviateNumber = (number: number, precision: number) => {
let tier = (Math.log10(number) / 3) | 0;
let scaled = number;
let suffix = SI_SYMBOL[tier];
if (tier !== 0) {
let scale = Math.pow(10, tier * 3);
scaled = number / scale;
}
return scaled.toFixed(precision) + suffix;
};
export const formatAmount = (
val: number,
precision: number = 6,
abbr: boolean = true,
) => (abbr ? abbreviateNumber(val, precision) : val.toFixed(precision));
export function formatTokenAmount(
account?: TokenAccount | number | BN,
mint?: MintInfo,
rate: number = 1.0,
prefix = '',
suffix = '',
precision = 6,
abbr = false,
): string {
if (!account) {
return '';
}
return `${[prefix]}${formatAmount(
fromLamports(account, mint, rate),
precision,
abbr,
)}${suffix}`;
}
export const formatUSD = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
const numberFormater = new Intl.NumberFormat('en-US', {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export const formatNumber = {
format: (val?: number) => {
if (!val) {
return '--';
}
return numberFormater.format(val);
},
};
export const formatPct = new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export function convert(
account?: TokenAccount | number,
mint?: MintInfo,
rate: number = 1.0,
): number {
if (!account) {
return 0;
}
const amount =
typeof account === 'number' ? account : account.info.amount?.toNumber();
const precision = Math.pow(10, mint?.decimals || 0);
let result = (amount / precision) * rate;
return result;
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@ -0,0 +1 @@
export * as solong_adapter from './solong_adapter';

View File

@ -0,0 +1,108 @@
import EventEmitter from 'eventemitter3';
import { PublicKey, Transaction } from '@solana/web3.js';
import { notify } from '../../utils/notifications';
import { WalletAdapter } from '@solana/wallet-base';
type PhantomEvent = 'disconnect' | 'connect';
type PhantomRequestMethod =
| 'connect'
| 'disconnect'
| 'signTransaction'
| 'signAllTransactions';
interface PhantomProvider {
publicKey?: PublicKey;
isConnected?: boolean;
autoApprove?: boolean;
signTransaction: (transaction: Transaction) => Promise<Transaction>;
signAllTransactions: (transactions: Transaction[]) => Promise<Transaction[]>;
connect: () => Promise<void>;
disconnect: () => Promise<void>;
on: (event: PhantomEvent, handler: (args: any) => void) => void;
request: (method: PhantomRequestMethod, params: any) => Promise<any>;
}
export class PhantomWalletAdapter
extends EventEmitter
implements WalletAdapter {
_provider: PhantomProvider | undefined;
_cachedCorrectKey?: PublicKey;
constructor() {
super();
this.connect = this.connect.bind(this);
}
get connected() {
return this._provider?.isConnected || false;
}
get autoApprove() {
return this._provider?.autoApprove || false;
}
async signAllTransactions(
transactions: Transaction[],
): Promise<Transaction[]> {
if (!this._provider) {
return transactions;
}
return this._provider.signAllTransactions(transactions);
}
get publicKey() {
// Due to weird phantom bug where their public key isnt quite like ours
if (!this._cachedCorrectKey && this._provider?.publicKey)
this._cachedCorrectKey = new PublicKey(
this._provider.publicKey.toBase58(),
);
return this._cachedCorrectKey || null;
}
async signTransaction(transaction: Transaction) {
if (!this._provider) {
return transaction;
}
return this._provider.signTransaction(transaction);
}
connect = async () => {
if (this._provider) {
return;
}
let provider: PhantomProvider;
if ((window as any)?.solana?.isPhantom) {
provider = (window as any).solana;
} else {
window.open('https://phantom.app/', '_blank');
notify({
message: 'Phantom Error',
description: 'Please install Phantom wallet from Chrome ',
});
return;
}
provider.on('connect', () => {
this._provider = provider;
this.emit('connect');
});
if (!provider.isConnected) {
await provider.connect();
}
this._provider = provider;
this.emit('connect');
};
disconnect() {
if (this._provider) {
this._provider.disconnect();
this._provider = undefined;
this.emit('disconnect');
}
}
}

View File

@ -0,0 +1,62 @@
import EventEmitter from "eventemitter3";
import {PublicKey, Transaction} from "@solana/web3.js";
import { WalletAdapter } from "@solana/wallet-base";
import { notify } from "../../utils/notifications";
export class SolongWalletAdapter extends EventEmitter implements WalletAdapter {
_publicKey: PublicKey | null;
_onProcess: boolean;
constructor() {
super();
this._publicKey = null;
this._onProcess = false;
this.connect = this.connect.bind(this);
}
get publicKey() {
return this._publicKey;
}
async signTransaction(transaction: Transaction) {
return (window as any).solong.signTransaction(transaction);
}
async signAllTransactions(transactions: Transaction[]) {
return transactions;
}
connect() {
if (this._onProcess) {
return;
}
if ((window as any).solong === undefined) {
notify({
message: "Solong Error",
description: "Please install solong wallet from Chrome ",
});
return;
}
this._onProcess = true;
(window as any).solong
.selectAccount()
.then((account: any) => {
this._publicKey = new PublicKey(account);
this.emit("connect", this._publicKey);
})
.catch(() => {
this.disconnect();
})
.finally(() => {
this._onProcess = false;
});
}
disconnect() {
if (this._publicKey) {
this._publicKey = null;
this.emit("disconnect");
}
}
}

View File

@ -0,0 +1,57 @@
import EventEmitter from "eventemitter3";
import { PublicKey } from "@solana/web3.js";
import { notify } from "../utils/notifications";
export class SolongAdapter extends EventEmitter {
_publicKey: any;
_onProcess: boolean;
constructor(providerUrl: string, network: string) {
super();
this._publicKey = null;
this._onProcess = false;
this.connect = this.connect.bind(this);
}
get publicKey() {
return this._publicKey;
}
async signTransaction(transaction: any) {
return (window as any).solong.signTransaction(transaction);
}
connect() {
if (this._onProcess) {
return;
}
if ((window as any).solong === undefined) {
notify({
message: "Solong Error",
description: "Please install solong wallet from Chrome ",
});
return;
}
this._onProcess = true;
(window as any).solong
.selectAccount()
.then((account: any) => {
this._publicKey = new PublicKey(account);
this.emit("connect", this._publicKey);
})
.catch(() => {
this.disconnect();
})
.finally(() => {
this._onProcess = false;
});
}
disconnect() {
if (this._publicKey) {
this._publicKey = null;
this.emit("disconnect");
}
}
}

View File

@ -0,0 +1,98 @@
import EventEmitter from "eventemitter3"
import { Keypair, PublicKey, Transaction } from "@solana/web3.js"
import { WalletAdapter } from "@solana/wallet-base"
import OpenLogin from "@toruslabs/openlogin"
import { getED25519Key } from "@toruslabs/openlogin-ed25519"
const getSolanaPrivateKey = (openloginKey: string)=>{
const { sk } = getED25519Key(openloginKey)
return sk
}
export class TorusWalletAdapter extends EventEmitter implements WalletAdapter {
_provider: OpenLogin | undefined;
endpoint: string;
providerUrl: string;
account: Keypair | undefined;
image: string = '';
name: string = '';
constructor(providerUrl: string, endpoint: string) {
super()
this.connect = this.connect.bind(this)
this.endpoint = endpoint;
this.providerUrl = providerUrl;
}
async signAllTransactions(transactions: Transaction[]): Promise<Transaction[]> {
if(this.account) {
let account = this.account;
transactions.forEach(t => t.partialSign(account));
}
return transactions
}
get publicKey() {
return this.account?.publicKey || null;
}
async signTransaction(transaction: Transaction) {
if(this.account) {
transaction.partialSign(this.account)
}
return transaction
}
connect = async () => {
this._provider = new OpenLogin({
clientId: process.env.REACT_APP_CLIENT_ID || 'BKBTX-SmaEFGddZQrwqd65YFoImRQLca_Tj2IdmKyD2UbDpzrtN2WQ-NYLuej6gP0DfF3jSpEkI13wPt1uPedm0',
network: "mainnet", // mainnet, testnet, development
uxMode: 'popup'
});
try {
await this._provider.init();
} catch (ex) {
console.error('init failed', ex)
}
console.error(this._provider?.state.store);
if (this._provider.privKey) {
const privateKey = this._provider.privKey;
console.log(privateKey);
const secretKey = getSolanaPrivateKey(privateKey);
console.log(secretKey);
this.account = Keypair.fromSecretKey(secretKey);
} else {
try {
const { privKey } = await this._provider.login();
console.log(privKey);
const secretKey = getSolanaPrivateKey(privKey);
console.log(secretKey);
this.account = Keypair.fromSecretKey(secretKey);
} catch(ex) {
console.error('login failed', ex);
}
}
console.log(this.account?.publicKey.toBase58());
this.name = this._provider?.state.store.get('name');;
this.image = this._provider?.state.store.get('profileImage');;
this.emit("connect");
}
disconnect = async () => {
console.log("Disconnecting...")
if (this._provider) {
await this._provider.logout();
await this._provider._cleanup();
this._provider = undefined;
this.emit("disconnect");
}
}
}

View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2019",
"outDir": "./dist/lib",
"rootDir": "./src",
"composite": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": ["types", "../../types", "../../node_modules/@types"]
},
"include": ["src/**/*"],
"exclude": ["src/**/*.test.ts", "**/node_modules"]
}

View File

@ -0,0 +1,9 @@
{
"env": {
"browser": true
},
"globals": {
"__PATH_PREFIX__": false,
"___emitter": false
}
}

View File

@ -0,0 +1,9 @@
// prefer default export if available
const preferDefault = m => (m && m.default) || m
exports.components = {
"component---src-pages-404-tsx": () => import("./../../../src/pages/404.tsx" /* webpackChunkName: "component---src-pages-404-tsx" */),
"component---src-pages-contact-tsx": () => import("./../../../src/pages/contact.tsx" /* webpackChunkName: "component---src-pages-contact-tsx" */),
"component---src-pages-index-tsx": () => import("./../../../src/pages/index.tsx" /* webpackChunkName: "component---src-pages-index-tsx" */)
}

View File

@ -0,0 +1,11 @@
// prefer default export if available
const preferDefault = m => (m && m.default) || m
exports.components = {
"component---src-pages-404-tsx": preferDefault(require("/Users/jprince/Documents/other/metaplex/js/packages/metaplex/src/pages/404.tsx")),
"component---src-pages-contact-tsx": preferDefault(require("/Users/jprince/Documents/other/metaplex/js/packages/metaplex/src/pages/contact.tsx")),
"component---src-pages-index-tsx": preferDefault(require("/Users/jprince/Documents/other/metaplex/js/packages/metaplex/src/pages/index.tsx"))
}

View File

@ -0,0 +1,10 @@
module.exports = [{
plugin: require('../../../node_modules/gatsby-plugin-image/gatsby-browser.js'),
options: {"plugins":[]},
},{
plugin: require('../../../node_modules/gatsby-plugin-manifest/gatsby-browser.js'),
options: {"plugins":[],"background_color":"#000000","icon":"src/images/icon.png","legacy":true,"theme_color_in_head":true,"cache_busting_mode":"query","crossOrigin":"anonymous","include_favicon":true,"cacheDigest":"7df75940c5c4611dd59c4b1856069d9e"},
},{
plugin: require('../gatsby-browser.js'),
options: {"plugins":[]},
}]

View File

@ -0,0 +1,55 @@
const plugins = require(`./api-runner-browser-plugins`)
const {
getResourceURLsForPathname,
loadPage,
loadPageSync,
} = require(`./loader`).publicLoader
exports.apiRunner = (api, args = {}, defaultReturn, argTransform) => {
// Hooks for gatsby-cypress's API handler
if (process.env.CYPRESS_SUPPORT) {
if (window.___apiHandler) {
window.___apiHandler(api)
} else if (window.___resolvedAPIs) {
window.___resolvedAPIs.push(api)
} else {
window.___resolvedAPIs = [api]
}
}
let results = plugins.map(plugin => {
if (!plugin.plugin[api]) {
return undefined
}
args.getResourceURLsForPathname = getResourceURLsForPathname
args.loadPage = loadPage
args.loadPageSync = loadPageSync
const result = plugin.plugin[api](args, plugin.options)
if (result && argTransform) {
args = argTransform({ args, result, plugin })
}
return result
})
// Filter out undefined results.
results = results.filter(result => typeof result !== `undefined`)
if (results.length > 0) {
return results
} else if (defaultReturn) {
return [defaultReturn]
} else {
return []
}
}
exports.apiRunnerAsync = (api, args, defaultReturn) =>
plugins.reduce(
(previous, next) =>
next.plugin[api]
? previous.then(() => next.plugin[api](args, next.options))
: previous,
Promise.resolve()
)

View File

@ -0,0 +1,65 @@
var plugins = [{
name: 'gatsby-plugin-image',
plugin: require('/Users/jprince/Documents/other/metaplex/js/node_modules/gatsby-plugin-image/gatsby-ssr'),
options: {"plugins":[]},
},{
name: 'gatsby-plugin-react-helmet',
plugin: require('/Users/jprince/Documents/other/metaplex/js/node_modules/gatsby-plugin-react-helmet/gatsby-ssr'),
options: {"plugins":[]},
},{
name: 'gatsby-plugin-manifest',
plugin: require('/Users/jprince/Documents/other/metaplex/js/node_modules/gatsby-plugin-manifest/gatsby-ssr'),
options: {"plugins":[],"background_color":"#000000","icon":"src/images/icon.png","legacy":true,"theme_color_in_head":true,"cache_busting_mode":"query","crossOrigin":"anonymous","include_favicon":true,"cacheDigest":"7df75940c5c4611dd59c4b1856069d9e"},
}]
// During bootstrap, we write requires at top of this file which looks like:
// var plugins = [
// {
// plugin: require("/path/to/plugin1/gatsby-ssr.js"),
// options: { ... },
// },
// {
// plugin: require("/path/to/plugin2/gatsby-ssr.js"),
// options: { ... },
// },
// ]
const apis = require(`./api-ssr-docs`)
// Run the specified API in any plugins that have implemented it
module.exports = (api, args, defaultReturn, argTransform) => {
if (!apis[api]) {
console.log(`This API doesn't exist`, api)
}
// Run each plugin in series.
// eslint-disable-next-line no-undef
let results = plugins.map(plugin => {
if (!plugin.plugin[api]) {
return undefined
}
try {
const result = plugin.plugin[api](args, plugin.options)
if (result && argTransform) {
args = argTransform({ args, result })
}
return result
} catch (e) {
if (plugin.name !== `default-site-plugin`) {
// default-site-plugin is user code and will print proper stack trace,
// so no point in annotating error message pointing out which plugin is root of the problem
e.message += ` (from plugin: ${plugin.name})`
}
throw e
}
})
// Filter out undefined results.
results = results.filter(result => typeof result !== `undefined`)
if (results.length > 0) {
return results
} else {
return [defaultReturn]
}
}

View File

@ -0,0 +1,205 @@
/**
* Object containing options defined in `gatsby-config.js`
* @typedef {object} pluginOptions
*/
/**
* Replace the default server renderer. This is useful for integration with
* Redux, css-in-js libraries, etc. that need custom setups for server
* rendering.
* @param {object} $0
* @param {string} $0.pathname The pathname of the page currently being rendered.
* @param {ReactNode} $0.bodyComponent The React element to be rendered as the page body
* @param {function} $0.replaceBodyHTMLString Call this with the HTML string
* you render. **WARNING** if multiple plugins implement this API it's the
* last plugin that "wins". TODO implement an automated warning against this.
* @param {function} $0.setHeadComponents Takes an array of components as its
* first argument which are added to the `headComponents` array which is passed
* to the `html.js` component.
* @param {function} $0.setHtmlAttributes Takes an object of props which will
* spread into the `<html>` component.
* @param {function} $0.setBodyAttributes Takes an object of props which will
* spread into the `<body>` component.
* @param {function} $0.setPreBodyComponents Takes an array of components as its
* first argument which are added to the `preBodyComponents` array which is passed
* to the `html.js` component.
* @param {function} $0.setPostBodyComponents Takes an array of components as its
* first argument which are added to the `postBodyComponents` array which is passed
* to the `html.js` component.
* @param {function} $0.setBodyProps Takes an object of data which
* is merged with other body props and passed to `html.js` as `bodyProps`.
* @param {pluginOptions} pluginOptions
* @example
* // From gatsby-plugin-glamor
* const { renderToString } = require("react-dom/server")
* const inline = require("glamor-inline")
*
* exports.replaceRenderer = ({ bodyComponent, replaceBodyHTMLString }) => {
* const bodyHTML = renderToString(bodyComponent)
* const inlinedHTML = inline(bodyHTML)
*
* replaceBodyHTMLString(inlinedHTML)
* }
*/
exports.replaceRenderer = true
/**
* Called after every page Gatsby server renders while building HTML so you can
* set head and body components to be rendered in your `html.js`.
*
* Gatsby does a two-pass render for HTML. It loops through your pages first
* rendering only the body and then takes the result body HTML string and
* passes it as the `body` prop to your `html.js` to complete the render.
*
* It's often handy to be able to send custom components to your `html.js`.
* For example, it's a very common pattern for React.js libraries that
* support server rendering to pull out data generated during the render to
* add to your HTML.
*
* Using this API over [`replaceRenderer`](#replaceRenderer) is preferable as
* multiple plugins can implement this API where only one plugin can take
* over server rendering. However, if your plugin requires taking over server
* rendering then that's the one to
* use
* @param {object} $0
* @param {string} $0.pathname The pathname of the page currently being rendered.
* @param {function} $0.setHeadComponents Takes an array of components as its
* first argument which are added to the `headComponents` array which is passed
* to the `html.js` component.
* @param {function} $0.setHtmlAttributes Takes an object of props which will
* spread into the `<html>` component.
* @param {function} $0.setBodyAttributes Takes an object of props which will
* spread into the `<body>` component.
* @param {function} $0.setPreBodyComponents Takes an array of components as its
* first argument which are added to the `preBodyComponents` array which is passed
* to the `html.js` component.
* @param {function} $0.setPostBodyComponents Takes an array of components as its
* first argument which are added to the `postBodyComponents` array which is passed
* to the `html.js` component.
* @param {function} $0.setBodyProps Takes an object of data which
* is merged with other body props and passed to `html.js` as `bodyProps`.
* @param {pluginOptions} pluginOptions
* @example
* // Import React so that you can use JSX in HeadComponents
* const React = require("react")
*
* const HtmlAttributes = {
* lang: "en"
* }
*
* const HeadComponents = [
* <script key="my-script" src="https://gatsby.dev/my-script" />
* ]
*
* const BodyAttributes = {
* "data-theme": "dark"
* }
*
* exports.onRenderBody = ({
* setHeadComponents,
* setHtmlAttributes,
* setBodyAttributes
* }, pluginOptions) => {
* setHtmlAttributes(HtmlAttributes)
* setHeadComponents(HeadComponents)
* setBodyAttributes(BodyAttributes)
* }
*/
exports.onRenderBody = true
/**
* Called after every page Gatsby server renders while building HTML so you can
* replace head components to be rendered in your `html.js`. This is useful if
* you need to reorder scripts or styles added by other plugins.
* @param {object} $0
* @param {string} $0.pathname The pathname of the page currently being rendered.
* @param {Array<ReactNode>} $0.getHeadComponents Returns the current `headComponents` array.
* @param {function} $0.replaceHeadComponents Takes an array of components as its
* first argument which replace the `headComponents` array which is passed
* to the `html.js` component. **WARNING** if multiple plugins implement this
* API it's the last plugin that "wins".
* @param {Array<ReactNode>} $0.getPreBodyComponents Returns the current `preBodyComponents` array.
* @param {function} $0.replacePreBodyComponents Takes an array of components as its
* first argument which replace the `preBodyComponents` array which is passed
* to the `html.js` component. **WARNING** if multiple plugins implement this
* API it's the last plugin that "wins".
* @param {Array<ReactNode>} $0.getPostBodyComponents Returns the current `postBodyComponents` array.
* @param {function} $0.replacePostBodyComponents Takes an array of components as its
* first argument which replace the `postBodyComponents` array which is passed
* to the `html.js` component. **WARNING** if multiple plugins implement this
* API it's the last plugin that "wins".
* @param {pluginOptions} pluginOptions
* @example
* // Move Typography.js styles to the top of the head section so they're loaded first.
* exports.onPreRenderHTML = ({ getHeadComponents, replaceHeadComponents }) => {
* const headComponents = getHeadComponents()
* headComponents.sort((x, y) => {
* if (x.key === 'TypographyStyle') {
* return -1
* } else if (y.key === 'TypographyStyle') {
* return 1
* }
* return 0
* })
* replaceHeadComponents(headComponents)
* }
*/
exports.onPreRenderHTML = true
/**
* Allow a plugin to wrap the page element.
*
* This is useful for setting wrapper components around pages that won't get
* unmounted on page changes. For setting Provider components, use [wrapRootElement](#wrapRootElement).
*
* _Note:_
* There is an equivalent hook in Gatsby's [Browser API](/docs/browser-apis/#wrapPageElement).
* It is recommended to use both APIs together.
* For example usage, check out [Using i18n](https://github.com/gatsbyjs/gatsby/tree/master/examples/using-i18n).
* @param {object} $0
* @param {ReactNode} $0.element The "Page" React Element built by Gatsby.
* @param {object} $0.props Props object used by page.
* @param {pluginOptions} pluginOptions
* @returns {ReactNode} Wrapped element
* @example
* const React = require("react")
* const Layout = require("./src/components/layout").default
*
* exports.wrapPageElement = ({ element, props }) => {
* // props provide same data to Layout as Page element will get
* // including location, data, etc - you don't need to pass it
* return <Layout {...props}>{element}</Layout>
* }
*/
exports.wrapPageElement = true
/**
* Allow a plugin to wrap the root element.
*
* This is useful to set up any Provider components that will wrap your application.
* For setting persistent UI elements around pages use [wrapPageElement](#wrapPageElement).
*
* _Note:_
* There is an equivalent hook in Gatsby's [Browser API](/docs/browser-apis/#wrapRootElement).
* It is recommended to use both APIs together.
* For example usage, check out [Using redux](https://github.com/gatsbyjs/gatsby/tree/master/examples/using-redux).
* @param {object} $0
* @param {ReactNode} $0.element The "Root" React Element built by Gatsby.
* @param {pluginOptions} pluginOptions
* @returns {ReactNode} Wrapped element
* @example
* const React = require("react")
* const { Provider } = require("react-redux")
*
* const createStore = require("./src/state/createStore")
* const store = createStore()
*
* exports.wrapRootElement = ({ element }) => {
* return (
* <Provider store={store}>
* {element}
* </Provider>
* )
* }
*/
exports.wrapRootElement = true

View File

@ -0,0 +1,244 @@
import React from "react"
import ReactDOM from "react-dom"
import domReady from "@mikaelkristiansson/domready"
import io from "socket.io-client"
import socketIo from "./socketIo"
import emitter from "./emitter"
import { apiRunner, apiRunnerAsync } from "./api-runner-browser"
import { setLoader, publicLoader } from "./loader"
import { Indicator } from "./loading-indicator/indicator"
import DevLoader from "./dev-loader"
import asyncRequires from "$virtual/async-requires"
// Generated during bootstrap
import matchPaths from "$virtual/match-paths.json"
import { LoadingIndicatorEventHandler } from "./loading-indicator"
import Root from "./root"
import { init as navigationInit } from "./navigation"
// ensure in develop we have at least some .css (even if it's empty).
// this is so there is no warning about not matching content-type when site doesn't include any regular css (for example when css-in-js is used)
// this also make sure that if all css is removed in develop we are not left with stale commons.css that have stale content
import "./blank.css"
// Enable fast-refresh for virtual sync-requires, gatsby-browser & navigation
// To ensure that our <Root /> component can hot reload in case anything below doesn't
// satisfy fast-refresh constraints
module.hot.accept([
`$virtual/async-requires`,
`./api-runner-browser`,
`./navigation`,
])
window.___emitter = emitter
if (
process.env.GATSBY_EXPERIMENTAL_CONCURRENT_FEATURES &&
!ReactDOM.unstable_createRoot
) {
throw new Error(
`The GATSBY_EXPERIMENTAL_CONCURRENT_FEATURES flag is not compatible with your React version. Please install "react@0.0.0-experimental-57768ef90" and "react-dom@0.0.0-experimental-57768ef90" or higher.`
)
}
const loader = new DevLoader(asyncRequires, matchPaths)
setLoader(loader)
loader.setApiRunner(apiRunner)
window.___loader = publicLoader
// Do dummy dynamic import so the jsonp __webpack_require__.e is added to the commons.js
// bundle. This ensures hot reloading doesn't break when someone first adds
// a dynamic import.
//
// Without this, the runtime breaks with a
// "TypeError: __webpack_require__.e is not a function"
// error.
export function notCalledFunction() {
return import(`./dummy`)
}
// Let the site/plugins run code very early.
apiRunnerAsync(`onClientEntry`).then(() => {
// Hook up the client to socket.io on server
const socket = socketIo()
if (socket) {
socket.on(`reload`, () => {
window.location.reload()
})
}
fetch(`/___services`)
.then(res => res.json())
.then(services => {
if (services.developstatusserver) {
let isRestarting = false
const parentSocket = io(
`${window.location.protocol}//${window.location.hostname}:${services.developstatusserver.port}`
)
parentSocket.on(`structured-log`, msg => {
if (
!isRestarting &&
msg.type === `LOG_ACTION` &&
msg.action.type === `DEVELOP` &&
msg.action.payload === `RESTART_REQUIRED` &&
window.confirm(
`The develop process needs to be restarted for the changes to ${msg.action.dirtyFile} to be applied.\nDo you want to restart the develop process now?`
)
) {
isRestarting = true
parentSocket.emit(`develop:restart`, () => {
window.location.reload()
})
}
if (
isRestarting &&
msg.type === `LOG_ACTION` &&
msg.action.type === `SET_STATUS` &&
msg.action.payload === `SUCCESS`
) {
isRestarting = false
window.location.reload()
}
})
// Prevents certain browsers spamming XHR 'ERR_CONNECTION_REFUSED'
// errors within the console, such as when exiting the develop process.
parentSocket.on(`disconnect`, () => {
console.warn(
`[socket.io] Disconnected. Unable to perform health-check.`
)
parentSocket.close()
})
}
})
/**
* Service Workers are persistent by nature. They stick around,
* serving a cached version of the site if they aren't removed.
* This is especially frustrating when you need to test the
* production build on your local machine.
*
* Let's warn if we find service workers in development.
*/
if (`serviceWorker` in navigator) {
navigator.serviceWorker.getRegistrations().then(registrations => {
if (registrations.length > 0)
console.warn(
`Warning: found one or more service workers present.`,
`If your site isn't behaving as expected, you might want to remove these.`,
registrations
)
})
}
const rootElement = document.getElementById(`___gatsby`)
const focusEl = document.getElementById(`gatsby-focus-wrapper`)
// Client only pages have any empty body so we just do a normal
// render to avoid React complaining about hydration mis-matches.
let defaultRenderer = ReactDOM.render
if (focusEl && focusEl.children.length) {
if (
process.env.GATSBY_EXPERIMENTAL_CONCURRENT_FEATURES &&
ReactDOM.unstable_createRoot
) {
defaultRenderer = ReactDOM.unstable_createRoot
} else {
defaultRenderer = ReactDOM.hydrate
}
}
const renderer = apiRunner(
`replaceHydrateFunction`,
undefined,
defaultRenderer
)[0]
let dismissLoadingIndicator
if (
process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND &&
process.env.GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR === `true`
) {
let indicatorMountElement
const showIndicatorTimeout = setTimeout(() => {
indicatorMountElement = document.createElement(
`first-render-loading-indicator`
)
document.body.append(indicatorMountElement)
ReactDOM.render(<Indicator />, indicatorMountElement)
}, 1000)
dismissLoadingIndicator = () => {
clearTimeout(showIndicatorTimeout)
if (indicatorMountElement) {
ReactDOM.unmountComponentAtNode(indicatorMountElement)
indicatorMountElement.remove()
}
}
}
Promise.all([
loader.loadPage(`/dev-404-page/`),
loader.loadPage(`/404.html`),
loader.loadPage(window.location.pathname),
]).then(() => {
navigationInit()
function onHydrated() {
apiRunner(`onInitialClientRender`)
// Render query on demand overlay
if (
process.env.GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR &&
process.env.GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR === `true`
) {
const indicatorMountElement = document.createElement(`div`)
indicatorMountElement.setAttribute(
`id`,
`query-on-demand-indicator-element`
)
document.body.append(indicatorMountElement)
if (renderer === ReactDOM.unstable_createRoot) {
renderer(indicatorMountElement).render(
<LoadingIndicatorEventHandler />
)
} else {
renderer(<LoadingIndicatorEventHandler />, indicatorMountElement)
}
}
}
function App() {
const onClientEntryRanRef = React.useRef(false)
React.useEffect(() => {
if (!onClientEntryRanRef.current) {
onClientEntryRanRef.current = true
onHydrated()
}
}, [])
return <Root />
}
domReady(() => {
if (dismissLoadingIndicator) {
dismissLoadingIndicator()
}
if (renderer === ReactDOM.unstable_createRoot) {
renderer(rootElement, {
hydrate: true,
}).render(<App />)
} else {
renderer(<App />, rootElement)
}
})
})
})

View File

@ -0,0 +1,9 @@
// prefer default export if available
const preferDefault = m => (m && m.default) || m
exports.components = {
"component---src-pages-404-tsx": () => import("./../../../src/pages/404.tsx" /* webpackChunkName: "component---src-pages-404-tsx" */),
"component---src-pages-contact-tsx": () => import("./../../../src/pages/contact.tsx" /* webpackChunkName: "component---src-pages-contact-tsx" */),
"component---src-pages-index-tsx": () => import("./../../../src/pages/index.tsx" /* webpackChunkName: "component---src-pages-index-tsx" */)
}

View File

@ -0,0 +1,212 @@
{
"stages": {
"develop": {
"plugins": [
{
"name": "babel-plugin-lodash",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/gatsby-plugin-image/dist/babel-plugin-parse-static-images.js",
"options": {
"cacheDir": "/Users/jprince/Documents/other/metaplex/js/packages/metaplex/.cache/caches/gatsby-plugin-image"
}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-optional-chaining/lib/index.js",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/lib/index.js",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-numeric-separator/lib/index.js",
"options": {}
}
],
"presets": [
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@emotion/babel-preset-css-prop/dist/emotion-babel-preset-css-prop.cjs.js",
"options": {
"sourceMap": true,
"autoLabel": "dev-only",
"plugins": [],
"labelFormat": "[local]",
"cssPropOptimization": true
}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/preset-typescript/lib/index.js",
"options": {
"plugins": [],
"allExtensions": false,
"isTSX": false,
"jsxPragma": "React"
}
}
],
"options": {
"cacheDirectory": true,
"sourceType": "unambiguous"
}
},
"develop-html": {
"plugins": [
{
"name": "babel-plugin-lodash",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/gatsby-plugin-image/dist/babel-plugin-parse-static-images.js",
"options": {
"cacheDir": "/Users/jprince/Documents/other/metaplex/js/packages/metaplex/.cache/caches/gatsby-plugin-image"
}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-optional-chaining/lib/index.js",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/lib/index.js",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-numeric-separator/lib/index.js",
"options": {}
}
],
"presets": [
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@emotion/babel-preset-css-prop/dist/emotion-babel-preset-css-prop.cjs.js",
"options": {
"sourceMap": true,
"autoLabel": "dev-only",
"plugins": [],
"labelFormat": "[local]",
"cssPropOptimization": true
}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/preset-typescript/lib/index.js",
"options": {
"plugins": [],
"allExtensions": false,
"isTSX": false,
"jsxPragma": "React"
}
}
],
"options": {
"cacheDirectory": true,
"sourceType": "unambiguous"
}
},
"build-html": {
"plugins": [
{
"name": "babel-plugin-lodash",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/gatsby-plugin-image/dist/babel-plugin-parse-static-images.js",
"options": {
"cacheDir": "/Users/jprince/Documents/other/metaplex/js/packages/metaplex/.cache/caches/gatsby-plugin-image"
}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-optional-chaining/lib/index.js",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/lib/index.js",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-numeric-separator/lib/index.js",
"options": {}
}
],
"presets": [
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@emotion/babel-preset-css-prop/dist/emotion-babel-preset-css-prop.cjs.js",
"options": {
"sourceMap": true,
"autoLabel": "dev-only",
"plugins": [],
"labelFormat": "[local]",
"cssPropOptimization": true
}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/preset-typescript/lib/index.js",
"options": {
"plugins": [],
"allExtensions": false,
"isTSX": false,
"jsxPragma": "React"
}
}
],
"options": {
"cacheDirectory": true,
"sourceType": "unambiguous"
}
},
"build-javascript": {
"plugins": [
{
"name": "babel-plugin-lodash",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/gatsby-plugin-image/dist/babel-plugin-parse-static-images.js",
"options": {
"cacheDir": "/Users/jprince/Documents/other/metaplex/js/packages/metaplex/.cache/caches/gatsby-plugin-image"
}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-optional-chaining/lib/index.js",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-nullish-coalescing-operator/lib/index.js",
"options": {}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/plugin-proposal-numeric-separator/lib/index.js",
"options": {}
}
],
"presets": [
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@emotion/babel-preset-css-prop/dist/emotion-babel-preset-css-prop.cjs.js",
"options": {
"sourceMap": true,
"autoLabel": "dev-only",
"plugins": [],
"labelFormat": "[local]",
"cssPropOptimization": true
}
},
{
"name": "/Users/jprince/Documents/other/metaplex/js/node_modules/@babel/preset-typescript/lib/index.js",
"options": {
"plugins": [],
"allExtensions": false,
"isTSX": false,
"jsxPragma": "React"
}
}
],
"options": {
"cacheDirectory": true,
"sourceType": "unambiguous"
}
}
},
"browserslist": [
">0.25%",
"not dead"
]
}

View File

@ -0,0 +1 @@
{"expireTime":9007200877154720000,"key":"transformer-remark-markdown-html-1e5365613ffaf3e4675c752c2ad1f127--","val":"<p>Metaplex lets artists and creators launch their own self-hosted NFT storefronts as easily as building a website. Create NFTs with drag-and-drop tools that connect directly to the <a href=\"https://solana.com\">Solana</a> blockchain.</p>"}

View File

@ -0,0 +1 @@
{"expireTime":9007200877154720000,"key":"transformer-remark-markdown-html-3a6cb0e794673b889a510e2f1cecdb32--","val":"<p>Metaplexs on-chain auctions offer powerful formats, with instant payouts and NFT transfers. Create Limited or Open Editions for your fans, or run a Tiered Auction where collectors bid for bundles of NFTs that are assigned at specific tiers.</p>"}

View File

@ -0,0 +1 @@
{"expireTime":9007200877154720000,"key":"transformer-remark-markdown-html-ast-52e44b6f5bb7a66a5dcbab38dcb13e4b--","val":{"type":"root","children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"NFT Images, audio, video, or media of any size are stored permanently on the blockchain with ","position":{"start":{"line":2,"column":1,"offset":1},"end":{"line":2,"column":94,"offset":94}}},{"type":"element","tagName":"a","properties":{"href":"https://www.arweave.org/"},"children":[{"type":"text","value":"Arweave","position":{"start":{"line":2,"column":95,"offset":95},"end":{"line":2,"column":102,"offset":102}}}],"position":{"start":{"line":2,"column":94,"offset":94},"end":{"line":2,"column":129,"offset":129}}},{"type":"text","value":". No external servers hosting media. Metaplex-minted NFTs are created forever, like a tattoo in cyberspace.","position":{"start":{"line":2,"column":129,"offset":129},"end":{"line":2,"column":236,"offset":236}}}],"position":{"start":{"line":2,"column":1,"offset":1},"end":{"line":2,"column":236,"offset":236}}}],"position":{"start":{"line":1,"column":1,"offset":0},"end":{"line":3,"column":1,"offset":237}}}}

View File

@ -0,0 +1 @@
{"expireTime":9007200877154720000,"key":"transformer-remark-markdown-html-ast-4b8fc29d98771b9058a482734924b095--","val":{"type":"root","children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"With an average minting cost of less than a dollar, and no platform fee, Metaplex is shifting the balance of power back to artists and creators.","position":{"start":{"line":2,"column":1,"offset":1},"end":{"line":2,"column":145,"offset":145}}}],"position":{"start":{"line":2,"column":1,"offset":1},"end":{"line":2,"column":145,"offset":145}}}],"position":{"start":{"line":1,"column":1,"offset":0},"end":{"line":3,"column":1,"offset":146}}}}

View File

@ -0,0 +1 @@
{"expireTime":9007200877154720000,"key":"transformer-remark-markdown-html-ast-3a6cb0e794673b889a510e2f1cecdb32--","val":{"type":"root","children":[{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Metaplexs on-chain auctions offer powerful formats, with instant payouts and NFT transfers. Create Limited or Open Editions for your fans, or run a Tiered Auction where collectors bid for bundles of NFTs that are assigned at specific tiers.","position":{"start":{"line":2,"column":1,"offset":1},"end":{"line":2,"column":242,"offset":242}}}],"position":{"start":{"line":2,"column":1,"offset":1},"end":{"line":2,"column":242,"offset":242}}}],"position":{"start":{"line":1,"column":1,"offset":0},"end":{"line":3,"column":1,"offset":243}}}}

Some files were not shown because too many files have changed in this diff Show More