├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .dockerignore ├── Dockerfile ├── Makefile ├── __init__.py ├── dev │ └── docker-tag ├── etc │ ├── asgi.py │ ├── entrypoint.sh │ ├── uwsgi.ini │ └── wsgi.py ├── helm │ ├── .helmignore │ ├── Chart.yaml │ ├── secrets.yaml │ ├── templates │ │ ├── _helpers.tpl │ │ ├── backend-service.yaml │ │ ├── backend.yaml │ │ ├── celery.yaml │ │ ├── ingress.yaml │ │ ├── rabbit-service.yaml │ │ ├── rabbit.yaml │ │ └── secret.yaml │ └── values.yaml ├── manage.py ├── nft_market │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── filters.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_use_big_integers.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── tasks.py │ │ ├── urls.py │ │ └── views.py │ ├── celery.py │ ├── contracts │ │ ├── __init__.py │ │ ├── assets │ │ │ ├── clear.py │ │ │ ├── escrow.py │ │ │ ├── helpers │ │ │ │ ├── __init__.py │ │ │ │ ├── parse.py │ │ │ │ └── state.py │ │ │ ├── manager.py │ │ │ └── proxy.py │ │ └── contracts.py │ ├── services │ │ ├── __init__.py │ │ └── algorand.py │ ├── settings.py │ ├── settings_dev.py │ ├── settings_test.py │ ├── urls.py │ ├── utils │ │ ├── __init__.py │ │ ├── authorization.py │ │ ├── constants.py │ │ ├── deployment.py │ │ ├── operations.py │ │ └── transactions.py │ └── wsgi.py ├── poetry.lock ├── pyproject.toml ├── pytest.ini └── tests │ ├── conftest.py │ └── test_compile.py ├── contracts ├── .eslintrc ├── algob.config.js ├── assets ├── common │ └── constants.js ├── package-lock.json ├── package.json ├── poetry.lock ├── pyproject.toml ├── scripts │ └── deploy.js ├── test │ ├── contract.mjs │ └── utils │ │ ├── assets.mjs │ │ ├── contract.mjs │ │ └── errors.mjs └── yarn.lock └── frontend ├── .editorconfig ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── algorand-logo.svg │ └── css │ │ ├── custom.css │ │ └── tailwind.css ├── components │ ├── AccountButton.vue │ ├── ActionButton.vue │ ├── AddressLink.vue │ ├── Alert.vue │ ├── AssetTitle.vue │ ├── ConnectToWalletButton.vue │ ├── FeaturedItem.vue │ ├── FileInput.vue │ ├── Footer.vue │ ├── NInput.vue │ ├── Navbar.vue │ ├── NumberInput.vue │ ├── PageContainer.vue │ ├── TopImage.vue │ ├── TransactionLink.vue │ ├── TransactionTable.vue │ ├── UpdateScheduler.vue │ ├── cards │ │ ├── AddAssetCard.vue │ │ ├── AssetCard.vue │ │ ├── AssetListCard.vue │ │ ├── DeployContractCard.vue │ │ └── ViewMore.vue │ └── modals │ │ ├── ActionModal.vue │ │ ├── BuyNowModal.vue │ │ ├── CancelModal.vue │ │ ├── MakeOfferModal.vue │ │ ├── ModalWrapper.vue │ │ ├── SelectAccountModal.vue │ │ ├── SelectWalletModal.vue │ │ ├── SellNowModal.vue │ │ └── SetAskPriceModal.vue ├── config │ └── index.js ├── main.js ├── mixins │ ├── authOnly.js │ ├── goBack.js │ └── staffOnly.js ├── router │ └── index.js ├── services │ ├── algoExplorer.js │ ├── algoSignerWallet.js │ ├── base.js │ ├── internal.js │ ├── myAlgoWallet.js │ └── walletManager.js ├── store │ ├── algorand │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js │ ├── index.js │ └── internal │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js ├── utils │ ├── constants.js │ ├── encoding.js │ ├── errors.js │ ├── eventBus.js │ ├── format.js │ ├── operations.js │ ├── precision.js │ ├── retry.js │ ├── transactions.js │ └── validation.js └── views │ ├── AddAsset.vue │ ├── AllItems.vue │ ├── AssetDetails.vue │ ├── AssetList.vue │ ├── DeployContract.vue │ ├── ForSale.vue │ ├── HighestBids.vue │ ├── RecentlyAdded.vue │ ├── UserAssetList.vue │ ├── UserBidList.vue │ └── UserCreatedAssetList.vue ├── tailwind.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | .idea/ 141 | 142 | node_modules/ 143 | backend/media/ 144 | 145 | secrets.yaml.dec 146 | contracts/artifacts 147 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ulam Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | .env 4 | .vscode 5 | .git 6 | .gitignore 7 | .coverage 8 | .pytest_cache 9 | .mypy_cache 10 | venv 11 | __pycache__ 12 | **/*.pyc 13 | **/__pycache__ 14 | media 15 | static 16 | *.egg-info 17 | pip-wheel-metadata 18 | tmp 19 | .log 20 | db.sqlite3 21 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.4-slim 2 | 3 | ENV LC_ALL=C.UTF-8 4 | ENV LANG=C.UTF-8 5 | 6 | RUN apt update -y && apt install -y git gcc wget 7 | RUN pip install poetry 8 | RUN poetry config virtualenvs.create false 9 | 10 | EXPOSE 80 11 | 12 | WORKDIR /app 13 | 14 | COPY ./pyproject.toml pyproject.toml 15 | COPY ./poetry.lock poetry.lock 16 | RUN poetry install 17 | 18 | ADD . . 19 | 20 | RUN python manage.py collectstatic --no-input 21 | 22 | ENTRYPOINT ["/app/etc/entrypoint.sh"] 23 | CMD ["web-prod"] 24 | -------------------------------------------------------------------------------- /backend/Makefile: -------------------------------------------------------------------------------- 1 | TAG = $(shell ./dev/docker-tag) 2 | IMAGE = ulamlabs/opennft:$(TAG) 3 | HELM_DEPLOYMENT = opennft 4 | 5 | K8S_CONTEXT = some-context 6 | 7 | .PHONY: help deploy 8 | 9 | all: image push 10 | 11 | help: 12 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' 13 | 14 | image: ## Build the docker image 15 | docker build -t opennft -t $(IMAGE) . 16 | 17 | push: ## push image 18 | docker push $(IMAGE) 19 | 20 | run: 21 | docker run -p 8000:80 -t opennft 22 | 23 | deploy: 24 | helm secrets upgrade --install \ 25 | -f ./helm//secrets.yaml \ 26 | $(HELM_DEPLOYMENT) ./helm/ \ 27 | --set image.tag=$(TAG) 28 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulamlabs/OpenNFT/fa4c2db63794a0c75a7e7eb7f63010baeeeee799/backend/__init__.py -------------------------------------------------------------------------------- /backend/dev/docker-tag: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # script used to prepare proper docker tag that consist of branch, latest tag, number of commits above that tag and sha, for example: master-v1.0.0-97-g9801f8e 4 | 5 | BRANCH=`git symbolic-ref HEAD --short 2>/dev/null` 6 | if [ "$BRANCH" = "" ] ; then 7 | BRANCH=`git branch -a --contains HEAD | sed -n 2p | awk '{ printf $1 }'` 8 | export BRANCH=${BRANCH#remotes/origin/} 9 | fi 10 | BRANCH=$(echo $BRANCH | cut -c -30) 11 | DESC_TAG=$(git describe --tags --always --abbrev=8) 12 | DOCKER_TAG=$BRANCH-$DESC_TAG 13 | DOCKER_TAG=${DOCKER_TAG//[^a-zA-Z_0-9-.]/_} 14 | 15 | echo $DOCKER_TAG 16 | -------------------------------------------------------------------------------- /backend/etc/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for nft_market project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nft_market.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/etc/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | action=$1 4 | shift 5 | 6 | case $action in 7 | web-prod) 8 | echo $GOOGLE_CREDENTIALS | base64 -d > /google_cred 9 | python manage.py migrate 10 | exec uwsgi /app/etc/uwsgi.ini "$@" 11 | ;; 12 | runserver) 13 | exec python manage.py runserver 0.0.0.0:8000 14 | ;; 15 | migrate) 16 | exec python manage.py migrate 17 | ;; 18 | makemigrations) 19 | exec python manage.py makemigrations 20 | ;; 21 | shell) 22 | exec python manage.py shell_plus 23 | ;; 24 | test) 25 | exec python manage.py test "$@" 26 | ;; 27 | celery) 28 | exec celery -A nft_market.celery worker --loglevel=DEBUG 29 | ;; 30 | *) 31 | exec $action "$@" 32 | ;; 33 | esac 34 | -------------------------------------------------------------------------------- /backend/etc/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http = 0.0.0.0:8000 3 | chdir = /app 4 | wsgi-file = /app/etc/wsgi.py 5 | pidfile = /tmp/uwsgi.pid 6 | master = 1 7 | processes = 2 8 | threads = 2 9 | static-map = /static=/app/static 10 | log-master=true 11 | -------------------------------------------------------------------------------- /backend/etc/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for nft_market project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nft_market.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /backend/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: nft-market 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /backend/helm/secrets.yaml: -------------------------------------------------------------------------------- 1 | secrets: 2 | DATABASE_URL: "" 3 | PURESTAKE_API_KEY: "" 4 | GOOGLE_CREDENTIALS: "" 5 | -------------------------------------------------------------------------------- /backend/helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "nft-market.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "nft-market.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "nft-market.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "nft-market.labels" -}} 37 | helm.sh/chart: {{ include "nft-market.chart" . }} 38 | {{ include "nft-market.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "nft-market.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "nft-market.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /backend/helm/templates/backend-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "nft-market.name" . }}-backend 5 | labels: 6 | app: {{ template "nft-market.name" . }}-backend 7 | chart: {{ template "nft-market.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | type: NodePort 12 | ports: 13 | - name: http 14 | port: 80 15 | targetPort: 8000 16 | protocol: TCP 17 | selector: 18 | app: {{ template "nft-market.name" . }}-backend 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /backend/helm/templates/backend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "nft-market.name" . }}-backend 5 | labels: 6 | {{- include "nft-market.labels" . | nindent 4 }} 7 | app: {{ template "nft-market.name" . }}-backend 8 | chart: {{ template "nft-market.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | replicas: {{ .Values.replicaCount }} 13 | selector: 14 | matchLabels: 15 | app: {{ template "nft-market.name" . }}-backend 16 | release: {{ .Release.Name }} 17 | template: 18 | metadata: 19 | {{- with .Values.podAnnotations }} 20 | annotations: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | labels: 24 | app: {{ template "nft-market.name" . }}-backend 25 | release: {{ .Release.Name }} 26 | spec: 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: {{ .Chart.Name }}-backend 35 | securityContext: 36 | {{- toYaml .Values.securityContext | nindent 12 }} 37 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 38 | imagePullPolicy: {{ .Values.image.pullPolicy }} 39 | ports: 40 | - name: http 41 | containerPort: 8000 42 | protocol: TCP 43 | livenessProbe: 44 | timeoutSeconds: 3 45 | httpGet: 46 | path: /health/ 47 | port: http 48 | readinessProbe: 49 | httpGet: 50 | path: /health/ 51 | port: http 52 | resources: 53 | {{- toYaml .Values.resources | nindent 12 }} 54 | env: 55 | {{- range $key, $value := .Values.vars }} 56 | - name: {{ $key }} 57 | value: {{ $value | quote }} 58 | {{- end }} 59 | {{- range $key, $value := .Values.secrets }} 60 | - name: {{ $key }} 61 | valueFrom: 62 | secretKeyRef: 63 | name: {{ template "nft-market.name" $ }} 64 | key: {{ $key }} 65 | {{- end }} 66 | {{- with .Values.nodeSelector }} 67 | nodeSelector: 68 | {{- toYaml . | nindent 8 }} 69 | {{- end }} 70 | {{- with .Values.affinity }} 71 | affinity: 72 | {{- toYaml . | nindent 8 }} 73 | {{- end }} 74 | {{- with .Values.tolerations }} 75 | tolerations: 76 | {{- toYaml . | nindent 8 }} 77 | {{- end }} 78 | -------------------------------------------------------------------------------- /backend/helm/templates/celery.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "nft-market.name" . }}-celery 5 | labels: 6 | {{- include "nft-market.labels" . | nindent 4 }} 7 | app: {{ template "nft-market.name" . }} 8 | chart: {{ template "nft-market.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | replicas: {{ .Values.celeryReplicaCount }} 13 | selector: 14 | matchLabels: 15 | app: {{ template "nft-market.name" . }}-celery 16 | release: {{ .Release.Name }} 17 | template: 18 | metadata: 19 | {{- with .Values.podAnnotations }} 20 | annotations: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | labels: 24 | app: {{ template "nft-market.name" . }}-celery 25 | release: {{ .Release.Name }} 26 | spec: 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: {{ .Chart.Name }}-celery 35 | args: ["celery"] 36 | securityContext: 37 | {{- toYaml .Values.securityContext | nindent 12 }} 38 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 39 | imagePullPolicy: {{ .Values.image.pullPolicy }} 40 | resources: 41 | {{- toYaml .Values.celeryResources | nindent 12 }} 42 | env: 43 | {{- range $key, $value := .Values.vars }} 44 | - name: {{ $key }} 45 | value: {{ $value | quote }} 46 | {{- end }} 47 | {{- range $key, $value := .Values.secrets }} 48 | - name: {{ $key }} 49 | valueFrom: 50 | secretKeyRef: 51 | name: {{ template "nft-market.name" $ }} 52 | key: {{ $key }} 53 | {{- end }} 54 | {{- with .Values.nodeSelector }} 55 | nodeSelector: 56 | {{- toYaml . | nindent 8 }} 57 | {{- end }} 58 | {{- with .Values.affinity }} 59 | affinity: 60 | {{- toYaml . | nindent 8 }} 61 | {{- end }} 62 | {{- with .Values.tolerations }} 63 | tolerations: 64 | {{- toYaml . | nindent 8 }} 65 | {{- end }} 66 | -------------------------------------------------------------------------------- /backend/helm/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: {{ template "nft-market.name" . }} 5 | labels: 6 | app: {{ template "nft-market.name" . }}-backend 7 | chart: {{ template "nft-market.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | annotations: 11 | kubernetes.io/ingress.class: "nginx" 12 | nginx.ingress.kubernetes.io/proxy-connect-timeout: "60" 13 | nginx.ingress.kubernetes.io/proxy-send-timeout: "60" 14 | nginx.ingress.kubernetes.io/proxy-read-timeout: "60" 15 | nginx.ingress.kubernetes.io/proxy-body-size: "15m" 16 | spec: 17 | rules: 18 | - host: {{ .Values.host_dns }} 19 | http: 20 | paths: 21 | - path: / 22 | backend: 23 | serviceName: {{ template "nft-market.name" . }}-backend 24 | servicePort: 80 25 | -------------------------------------------------------------------------------- /backend/helm/templates/rabbit-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "nft-market.name" . }}-rabbit 5 | labels: 6 | app: {{ template "nft-market.name" . }}-rabbit 7 | chart: {{ template "nft-market.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | type: NodePort 12 | ports: 13 | - name: rabbit 14 | port: 5672 15 | targetPort: 5672 16 | protocol: TCP 17 | selector: 18 | app: {{ template "nft-market.name" . }}-rabbit 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /backend/helm/templates/rabbit.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "nft-market.name" . }}-rabbit 5 | labels: 6 | {{- include "nft-market.labels" . | nindent 4 }} 7 | app: {{ template "nft-market.name" . }}-rabbit 8 | chart: {{ template "nft-market.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app: {{ template "nft-market.name" . }}-rabbit 16 | release: {{ .Release.Name }} 17 | template: 18 | metadata: 19 | {{- with .Values.podAnnotations }} 20 | annotations: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | labels: 24 | app: {{ template "nft-market.name" . }}-rabbit 25 | release: {{ .Release.Name }} 26 | spec: 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: {{ .Chart.Name }}-rabbit 35 | securityContext: 36 | {{- toYaml .Values.securityContext | nindent 12 }} 37 | image: rabbitmq 38 | imagePullPolicy: IfNotPresent 39 | ports: 40 | - name: rabbit 41 | containerPort: 5672 42 | protocol: TCP 43 | resources: 44 | {{- toYaml .Values.rabbitResources | nindent 12 }} 45 | env: 46 | {{- range $key, $value := .Values.vars }} 47 | - name: {{ $key }} 48 | value: {{ $value | quote }} 49 | {{- end }} 50 | {{- range $key, $value := .Values.secrets }} 51 | - name: {{ $key }} 52 | valueFrom: 53 | secretKeyRef: 54 | name: {{ template "nft-market.name" $ }} 55 | key: {{ $key | quote }} 56 | {{- end }} 57 | {{- with .Values.nodeSelector }} 58 | nodeSelector: 59 | {{- toYaml . | nindent 8 }} 60 | {{- end }} 61 | {{- with .Values.affinity }} 62 | affinity: 63 | {{- toYaml . | nindent 8 }} 64 | {{- end }} 65 | {{- with .Values.tolerations }} 66 | tolerations: 67 | {{- toYaml . | nindent 8 }} 68 | {{- end }} 69 | -------------------------------------------------------------------------------- /backend/helm/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | type: Opaque 3 | kind: Secret 4 | metadata: 5 | name: {{ template "nft-market.name" . }} 6 | data: 7 | {{- range $key, $value := .Values.secrets }} 8 | {{ $key }}: {{ $value | b64enc | quote }} 9 | {{- end }} 10 | -------------------------------------------------------------------------------- /backend/helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for nft-market. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | celeryReplicaCount: 1 7 | 8 | image: 9 | repository: ulamlabs/opennft 10 | pullPolicy: Always 11 | # Overrides the image tag whose default is the chart appVersion. 12 | tag: "" 13 | 14 | imagePullSecrets: [] 15 | nameOverride: "" 16 | fullnameOverride: "" 17 | 18 | podAnnotations: {} 19 | 20 | podSecurityContext: {} 21 | # fsGroup: 2000 22 | 23 | securityContext: {} 24 | # capabilities: 25 | # drop: 26 | # - ALL 27 | # readOnlyRootFilesystem: true 28 | # runAsNonRoot: true 29 | # runAsUser: 1000 30 | 31 | resources: {} 32 | # We usually recommend not to specify default resources and to leave this as a conscious 33 | # choice for the user. This also increases chances charts run on environments with little 34 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 35 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 36 | # limits: 37 | # cpu: 100m 38 | # memory: 128Mi 39 | # requests: 40 | # cpu: 100m 41 | # memory: 128Mi 42 | 43 | celeryResources: {} 44 | 45 | rabbitResources: {} 46 | 47 | nodeSelector: {} 48 | 49 | tolerations: [] 50 | 51 | affinity: {} 52 | 53 | vars: 54 | GOOGLE_APPLICATION_CREDENTIALS: /google_cred 55 | 56 | host_dns: "openft.ulam.io" 57 | 58 | secrets: {} 59 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nft_market.settings_dev") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /backend/nft_market/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulamlabs/OpenNFT/fa4c2db63794a0c75a7e7eb7f63010baeeeee799/backend/nft_market/__init__.py -------------------------------------------------------------------------------- /backend/nft_market/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulamlabs/OpenNFT/fa4c2db63794a0c75a7e7eb7f63010baeeeee799/backend/nft_market/api/__init__.py -------------------------------------------------------------------------------- /backend/nft_market/api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from nft_market.api.models import Asset, User, Operation 3 | 4 | 5 | class AssetAdmin(admin.ModelAdmin): 6 | list_display = ("unit_name", "name", "asset_id") 7 | 8 | 9 | class OperationAdmin(admin.ModelAdmin): 10 | list_display = ( 11 | "op_type", 12 | "sender", 13 | "tx_id", 14 | "is_valid", 15 | "is_pending", 16 | "is_executed", 17 | "block_number", 18 | "block_time", 19 | ) 20 | 21 | 22 | class UserAdmin(admin.ModelAdmin): 23 | pass 24 | 25 | 26 | admin.site.register(Asset, AssetAdmin) 27 | admin.site.register(Operation, OperationAdmin) 28 | admin.site.register(User, UserAdmin) 29 | -------------------------------------------------------------------------------- /backend/nft_market/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = "api" 6 | -------------------------------------------------------------------------------- /backend/nft_market/api/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django.db.models import Q 3 | from nft_market.api.models import Asset 4 | 5 | 6 | class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): 7 | pass 8 | 9 | 10 | class AssetFilter(django_filters.FilterSet): 11 | highest_bid__value__gt = django_filters.NumberFilter( 12 | field_name="highest_bid__value", lookup_expr="gt" 13 | ) 14 | price__gt = django_filters.NumberFilter(field_name="price", lookup_expr="gt") 15 | status__exclude = django_filters.CharFilter(field_name="status", exclude=True) 16 | application_id__in = NumberInFilter(field_name="application_id", lookup_expr="in") 17 | 18 | @property 19 | def qs(self): 20 | base_queryset = super().qs 21 | 22 | asset_id__in = self.data.get("asset_id__in") 23 | asset_id__in = asset_id__in.split(",") if asset_id__in else None 24 | owner_address = self.data.get("owner_address") 25 | if asset_id__in and owner_address: 26 | return base_queryset.filter( 27 | Q(asset_id__in=asset_id__in) | Q(owner_address=owner_address) 28 | ) 29 | elif asset_id__in: 30 | return base_queryset.filter(asset_id__in=asset_id__in) 31 | elif owner_address: 32 | return base_queryset.filter(owner_address=owner_address) 33 | 34 | return base_queryset 35 | 36 | class Meta: 37 | model = Asset 38 | fields = ("status", "creator_address") 39 | -------------------------------------------------------------------------------- /backend/nft_market/api/migrations/0002_use_big_integers.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-05-14 15:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="asset", 15 | name="application_id", 16 | field=models.PositiveBigIntegerField(blank=True, null=True, unique=True), 17 | ), 18 | migrations.AlterField( 19 | model_name="asset", 20 | name="asset_id", 21 | field=models.PositiveBigIntegerField(unique=True), 22 | ), 23 | migrations.AlterField( 24 | model_name="asset", 25 | name="last_round", 26 | field=models.PositiveBigIntegerField(default=0), 27 | ), 28 | migrations.AlterField( 29 | model_name="operation", 30 | name="block_number", 31 | field=models.PositiveBigIntegerField(blank=True, null=True), 32 | ), 33 | migrations.AlterField( 34 | model_name="operation", 35 | name="value", 36 | field=models.PositiveBigIntegerField(blank=True, null=True), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /backend/nft_market/api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulamlabs/OpenNFT/fa4c2db63794a0c75a7e7eb7f63010baeeeee799/backend/nft_market/api/migrations/__init__.py -------------------------------------------------------------------------------- /backend/nft_market/api/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | from django.db.models import Q 5 | 6 | 7 | class TimeStampMixin(models.Model): 8 | created_at = models.DateTimeField(auto_now_add=True) 9 | updated_at = models.DateTimeField(auto_now=True) 10 | 11 | class Meta: 12 | abstract = True 13 | 14 | 15 | class Asset(TimeStampMixin, models.Model): 16 | class AssetStatus(models.TextChoices): 17 | DEPLOYED_ASSET = "DA", "Deployed asset" 18 | DEPLOYED_CONTRACT = "DC", "Deployed contract" 19 | READY = "RD", "Ready to use" 20 | 21 | guid = models.UUIDField(unique=True, db_index=True) 22 | unit_name = models.CharField( 23 | max_length=8, unique=True, verbose_name="ticker symbol" 24 | ) 25 | name = models.CharField(max_length=32, unique=True, verbose_name="asset name") 26 | asset_id = models.PositiveBigIntegerField(unique=True) 27 | description = models.TextField() 28 | application_id = models.PositiveBigIntegerField(unique=True, blank=True, null=True) 29 | escrow_address = models.CharField(max_length=58, unique=True, blank=True, null=True) 30 | holding_address = models.CharField(max_length=58, blank=True, null=True) 31 | creator_address = models.CharField(max_length=58) 32 | status = models.CharField( 33 | max_length=2, 34 | choices=AssetStatus.choices, 35 | default=AssetStatus.DEPLOYED_ASSET, 36 | ) 37 | image = models.FileField() 38 | last_round = models.PositiveBigIntegerField(default=0) 39 | last_check = models.DateTimeField(blank=True, null=True) 40 | 41 | class Meta: 42 | verbose_name = "Asset" 43 | 44 | 45 | class User(TimeStampMixin, models.Model): 46 | first_name = models.CharField(max_length=50, blank=True, null=True) 47 | last_name = models.CharField(max_length=50, blank=True, null=True) 48 | address = models.CharField(max_length=58) 49 | is_staff = models.BooleanField() 50 | 51 | 52 | def get_placeholder_tx_id(): 53 | return uuid.uuid4().hex 54 | 55 | 56 | class Operation(TimeStampMixin, models.Model): 57 | class OperationType(models.TextChoices): 58 | ASK = "ASK", "Ask" 59 | BID = "BID", "Bid" 60 | BUY_NOW = ( 61 | "BUY_NOW", 62 | "Buy Now", 63 | ) 64 | SELL_NOW = "SELL_NOW", "Sell Now" 65 | 66 | op_type = models.CharField( 67 | max_length=8, 68 | choices=OperationType.choices, 69 | ) 70 | value = models.PositiveBigIntegerField(blank=True, null=True) 71 | tx_id = models.CharField(max_length=58, blank=True, null=True) 72 | sender = models.CharField(max_length=58, blank=True, null=True) 73 | # Other side of the transaction e.g. buyer in case of SELL_NOW tx 74 | account = models.CharField(max_length=58, blank=True, null=True) 75 | asset = models.ForeignKey(Asset, on_delete=models.CASCADE, blank=True, null=True) 76 | # Determines whether offer is still valid 77 | is_valid = models.BooleanField(default=True) 78 | is_executed = models.BooleanField(default=False) 79 | is_pending = models.BooleanField(default=False) # Has to be sent 80 | block_number = models.PositiveBigIntegerField(blank=True, null=True) 81 | block_time = models.DateTimeField(blank=True, null=True) 82 | blob = models.BinaryField(blank=True, null=True) # Raw tx data 83 | 84 | class Meta: 85 | constraints = [ 86 | models.UniqueConstraint( 87 | fields=["tx_id"], 88 | name="unique__name__when__tx_id__not_null", 89 | condition=Q(tx_id__isnull=False), 90 | ), 91 | ] 92 | -------------------------------------------------------------------------------- /backend/nft_market/api/serializers.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from nft_market.api.models import Asset, User, Operation 4 | from rest_framework import serializers 5 | from rest_framework.exceptions import ValidationError 6 | 7 | 8 | class CompileEscrowSerializer(serializers.Serializer): 9 | app_id = serializers.IntegerField() 10 | nft_id = serializers.IntegerField() 11 | usdc_id = serializers.IntegerField() 12 | 13 | 14 | class CompileProxySerializer(serializers.Serializer): 15 | proxy_id = serializers.IntegerField() 16 | 17 | 18 | class SendTxSerializer(serializers.Serializer): 19 | blob = serializers.CharField() 20 | 21 | 22 | class SendOperationSerializer(serializers.Serializer): 23 | blob = serializers.CharField() 24 | operation = serializers.ChoiceField(choices=Operation.OperationType.choices) 25 | 26 | 27 | class CreateAssetSerializer(serializers.Serializer): 28 | asset_id = serializers.CharField() 29 | image = serializers.ImageField() 30 | description = serializers.CharField() 31 | 32 | 33 | class ContractTxSerializer(serializers.Serializer): 34 | app_id = serializers.IntegerField() 35 | tx_id = serializers.CharField() 36 | 37 | 38 | class ValidateAssetSerializer(serializers.ModelSerializer): 39 | class Meta: 40 | model = Asset 41 | fields = ( 42 | "unit_name", 43 | "name", 44 | "description", 45 | ) 46 | 47 | 48 | def validate_file_size(value): 49 | limit = 4 * 1024 * 1024 50 | if value.size > limit: 51 | raise ValidationError("File too large. Size should not exceed 4 MiB.") 52 | 53 | 54 | class AssetSerializer(serializers.ModelSerializer): 55 | guid = serializers.CharField() 56 | owner_address = serializers.CharField(read_only=True) 57 | image = serializers.ImageField(validators=[validate_file_size]) 58 | description = serializers.CharField() 59 | asset_id = serializers.IntegerField() 60 | application_id = serializers.IntegerField(read_only=True) 61 | creator_address = serializers.CharField() 62 | escrow_address = serializers.CharField(read_only=True) 63 | status = serializers.CharField(read_only=True) 64 | price = serializers.IntegerField(read_only=True) 65 | highest_bid = serializers.SerializerMethodField() 66 | created_at = serializers.DateTimeField(read_only=True) 67 | modified_at = serializers.DateTimeField(read_only=True) 68 | 69 | def validate_guid(self, value): 70 | try: 71 | return uuid.UUID(value) 72 | except Exception as exc: 73 | raise serializers.ValidationError(f"{type(exc).__name__}: {str(exc)}") 74 | 75 | def get_highest_bid(self, obj): 76 | if not hasattr(obj, "highest_bid__sender") or not hasattr( 77 | obj, "highest_bid__value" 78 | ): 79 | return None 80 | if obj.highest_bid__sender and obj.highest_bid__value: 81 | return { 82 | "sender": obj.highest_bid__sender, 83 | "value": obj.highest_bid__value, 84 | } 85 | return None 86 | 87 | class Meta: 88 | model = Asset 89 | fields = ( 90 | "unit_name", 91 | "name", 92 | "description", 93 | "guid", 94 | "creator_address", 95 | "owner_address", 96 | "image", 97 | "asset_id", 98 | "application_id", 99 | "escrow_address", 100 | "status", 101 | "price", 102 | "highest_bid", 103 | "created_at", 104 | "modified_at", 105 | "holding_address", 106 | ) 107 | 108 | 109 | class UserSerializer(serializers.ModelSerializer): 110 | address = serializers.CharField() 111 | 112 | class Meta: 113 | model = User 114 | fields = ( 115 | "address", 116 | "is_staff", 117 | ) 118 | 119 | 120 | class OperationSerializer(serializers.ModelSerializer): 121 | class Meta: 122 | model = Operation 123 | fields = ( 124 | "pk", 125 | "created_at", 126 | "op_type", 127 | "value", 128 | "tx_id", 129 | "sender", 130 | "is_valid", 131 | "is_pending", 132 | "is_executed", 133 | "block_time", 134 | "block_number", 135 | ) 136 | -------------------------------------------------------------------------------- /backend/nft_market/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework.routers import DefaultRouter 3 | 4 | from nft_market.api.views import ( 5 | ContractViewSet, 6 | AssetViewSet, 7 | TransactionViewSet, 8 | UserViewSet, 9 | OperationViewSet, 10 | get_csrf_token, 11 | ) 12 | 13 | router = DefaultRouter() 14 | router.register(r"contracts", ContractViewSet, basename="contract") 15 | router.register(r"assets", AssetViewSet, basename="asset") 16 | router.register(r"transactions", TransactionViewSet, basename="transaction") 17 | router.register(r"users", UserViewSet, basename="user") 18 | router.register(r"operations", OperationViewSet, basename="operation") 19 | 20 | 21 | urlpatterns = router.urls + [ 22 | path("token/", get_csrf_token), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/nft_market/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nft_market.settings") 7 | 8 | app = Celery("nft_market") 9 | 10 | # Using a string here means the worker doesn't have to serialize 11 | # the configuration object to child processes. 12 | # - namespace='CELERY' means all celery-related configuration keys 13 | # should have a `CELERY_` prefix. 14 | app.config_from_object("django.conf:settings", namespace="CELERY") 15 | 16 | # Load task modules from all registered Django app configs. 17 | app.autodiscover_tasks() 18 | -------------------------------------------------------------------------------- /backend/nft_market/contracts/__init__.py: -------------------------------------------------------------------------------- 1 | from .contracts import * 2 | -------------------------------------------------------------------------------- /backend/nft_market/contracts/assets/clear.py: -------------------------------------------------------------------------------- 1 | from pyteal import * 2 | 3 | if __name__ == "__main__": 4 | from helpers.state import GlobalState, LocalState 5 | else: 6 | from .helpers.state import GlobalState, LocalState 7 | 8 | 9 | def clear(): 10 | BID_PRICE = LocalState("B") 11 | 12 | return Seq( 13 | [ 14 | BID_PRICE.put(Int(0)), 15 | ] 16 | ) 17 | 18 | 19 | if __name__ == "__main__": 20 | print(compileTeal(clear(), Mode.Application)) 21 | -------------------------------------------------------------------------------- /backend/nft_market/contracts/assets/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulamlabs/OpenNFT/fa4c2db63794a0c75a7e7eb7f63010baeeeee799/backend/nft_market/contracts/assets/helpers/__init__.py -------------------------------------------------------------------------------- /backend/nft_market/contracts/assets/helpers/parse.py: -------------------------------------------------------------------------------- 1 | # Original file: https://github.com/scale-it/algorand-builder/blob/master/examples/algobpy/parse.py 2 | import yaml 3 | 4 | 5 | def parse_args(args, sc_param): 6 | # decode external parameter and update current values. 7 | # (if an external paramter is passed) 8 | try: 9 | param = yaml.safe_load(args) 10 | for key, value in param.items(): 11 | sc_param[key] = value 12 | return sc_param 13 | except yaml.YAMLError as exc: 14 | print(exc) 15 | -------------------------------------------------------------------------------- /backend/nft_market/contracts/assets/helpers/state.py: -------------------------------------------------------------------------------- 1 | from pyteal import * 2 | 3 | 4 | class State: 5 | """ 6 | Wrapper around state vars. 7 | """ 8 | 9 | def __init__(self, name: str): 10 | self._name = name 11 | 12 | def put(self, value) -> App: 13 | raise NotImplementedError 14 | 15 | def get(self) -> App: 16 | raise NotImplementedError 17 | 18 | 19 | class LocalState(State): 20 | def put(self, value) -> App: 21 | return App.localPut(Int(0), Bytes(self._name), value) 22 | 23 | def get(self) -> App: 24 | return App.localGet(Int(0), Bytes(self._name)) 25 | 26 | 27 | class GlobalState(State): 28 | def put(self, value) -> App: 29 | return App.globalPut(Bytes(self._name), value) 30 | 31 | def get(self) -> App: 32 | return App.globalGet(Bytes(self._name)) 33 | -------------------------------------------------------------------------------- /backend/nft_market/contracts/assets/proxy.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from pyteal import * 4 | 5 | if __name__ == "__main__": 6 | from helpers.parse import parse_args 7 | else: 8 | from .helpers.parse import parse_args 9 | 10 | 11 | def proxy(proxy_id: int): 12 | return Seq( 13 | [ 14 | Assert(Int(proxy_id) == Int(proxy_id)), 15 | Return(Int(1)), 16 | ] 17 | ) 18 | 19 | 20 | if __name__ == "__main__": 21 | params = { 22 | "proxy_id": 123, 23 | "owner_address": "", 24 | } 25 | 26 | # Overwrite params if sys.argv[1] is passed 27 | if len(sys.argv) > 1: 28 | params = parse_args(sys.argv[1], params) 29 | 30 | print( 31 | compileTeal( 32 | proxy(params["proxy_id"]), 33 | Mode.Signature, 34 | ) 35 | ) 36 | -------------------------------------------------------------------------------- /backend/nft_market/contracts/contracts.py: -------------------------------------------------------------------------------- 1 | from pyteal import * 2 | 3 | from nft_market.contracts.assets.clear import clear 4 | from nft_market.contracts.assets.escrow import escrow 5 | from nft_market.contracts.assets.proxy import proxy 6 | from nft_market.contracts.assets.manager import ManagerContract 7 | 8 | 9 | def get_clear_teal(): 10 | return compileTeal(clear(), Mode.Application) 11 | 12 | 13 | def get_escrow_teal(app_id: int, usdc_id: int, nft_id: int): 14 | return compileTeal( 15 | escrow( 16 | app_id, 17 | usdc_id, 18 | nft_id, 19 | ), 20 | Mode.Signature, 21 | ) 22 | 23 | 24 | def get_proxy_teal(proxy_id): 25 | return compileTeal(proxy(proxy_id), Mode.Signature) 26 | 27 | 28 | def get_manager_teal(): 29 | return compileTeal(ManagerContract().get_contract(), Mode.Application) 30 | -------------------------------------------------------------------------------- /backend/nft_market/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulamlabs/OpenNFT/fa4c2db63794a0c75a7e7eb7f63010baeeeee799/backend/nft_market/services/__init__.py -------------------------------------------------------------------------------- /backend/nft_market/services/algorand.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import requests 4 | from algosdk.v2client.algod import AlgodClient 5 | from algosdk.v2client.indexer import IndexerClient 6 | from django.conf import settings 7 | 8 | PURESTAKE_ALGOD_URL = ( 9 | "https://testnet-algorand.api.purestake.io/ps2" 10 | if settings.USE_TESTNET 11 | else "https://mainnet-algorand.api.purestake.io/ps2" 12 | ) 13 | PURESTAKE_INDEXER_URL = ( 14 | "https://testnet-algorand.api.purestake.io/idx2" 15 | if settings.USE_TESTNET 16 | else "https://testnet-algorand.api.purestake.io/idx2" 17 | ) 18 | # We're using AlgoExplorer to look for transactions because it is more reliable than the official Algorand's indexer 19 | TXS_URL = ( 20 | "https://testnet.algoexplorerapi.io/idx2/v2/transactions" 21 | if settings.USE_TESTNET 22 | else "https://algoexplorerapi.io/idx2/v2/transactions" 23 | ) 24 | 25 | headers = {"X-API-Key": settings.PURESTAKE_API_KEY} 26 | algod = AlgodClient(settings.PURESTAKE_API_KEY, PURESTAKE_ALGOD_URL, headers) 27 | indexer = IndexerClient(settings.PURESTAKE_API_KEY, PURESTAKE_INDEXER_URL, headers) 28 | 29 | 30 | class Explorer: 31 | def search_transactions( 32 | self, 33 | limit=None, 34 | next_page=None, 35 | note_prefix=None, 36 | txn_type=None, 37 | sig_type=None, 38 | txid=None, 39 | min_round=None, 40 | max_round=None, 41 | asset_id=None, 42 | start_time=None, 43 | end_time=None, 44 | min_amount=None, 45 | max_amount=None, 46 | address=None, 47 | address_role=None, 48 | exclude_close_to=False, 49 | application_id=None, 50 | rekey_to=False, 51 | ): 52 | query = dict() 53 | if limit: 54 | query["limit"] = limit 55 | if next_page: 56 | query["next"] = next_page 57 | if note_prefix: 58 | query["note-prefix"] = base64.b64encode(note_prefix).decode() 59 | if txn_type: 60 | query["tx-type"] = txn_type 61 | if sig_type: 62 | query["sig-type"] = sig_type 63 | if txid: 64 | query["txid"] = txid 65 | if min_round: 66 | query["min-round"] = min_round 67 | if max_round: 68 | query["max-round"] = max_round 69 | if asset_id: 70 | query["asset-id"] = asset_id 71 | if end_time: 72 | query["before-time"] = end_time 73 | if start_time: 74 | query["after-time"] = start_time 75 | if min_amount: 76 | query["currency-greater-than"] = min_amount 77 | if max_amount: 78 | query["currency-less-than"] = max_amount 79 | if address: 80 | query["address"] = address 81 | if address_role: 82 | query["address-role"] = address_role 83 | if exclude_close_to: 84 | query["exclude-close-to"] = "true" 85 | if application_id: 86 | query["application-id"] = application_id 87 | if rekey_to: 88 | query["rekey-to"] = "true" 89 | r = requests.get( 90 | TXS_URL, 91 | params=query, 92 | headers={ 93 | "content-type": "application/json", 94 | }, 95 | ) 96 | return r.json() 97 | 98 | 99 | explorer = Explorer() 100 | -------------------------------------------------------------------------------- /backend/nft_market/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for nft_market project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | USE_TESTNET = os.environ.get("USE_TESTNET", "1") == "1" 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = os.environ.get( 25 | "SECRET_KEY", "#4vw_a2_1w&*=_(*3wm(3=650hi_!m#zxc_r4f4c$texam7rk+" 26 | ) 27 | 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = os.environ.get("DEBUG", "0") != "0" 30 | 31 | ALLOWED_HOSTS = ["*"] 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | "django.contrib.admin", 37 | "django.contrib.auth", 38 | "django.contrib.contenttypes", 39 | "django.contrib.sessions", 40 | "django.contrib.messages", 41 | "django.contrib.staticfiles", 42 | "rest_framework", 43 | "corsheaders", 44 | "django_filters", 45 | "nft_market.api", 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | "corsheaders.middleware.CorsMiddleware", 50 | "django.middleware.security.SecurityMiddleware", 51 | "django.contrib.sessions.middleware.SessionMiddleware", 52 | "django.middleware.common.CommonMiddleware", 53 | "django.middleware.csrf.CsrfViewMiddleware", 54 | "django.contrib.auth.middleware.AuthenticationMiddleware", 55 | "django.contrib.messages.middleware.MessageMiddleware", 56 | "corsheaders.middleware.CorsPostCsrfMiddleware", 57 | ] 58 | 59 | ROOT_URLCONF = "nft_market.urls" 60 | 61 | TEMPLATES = [ 62 | { 63 | "BACKEND": "django.template.backends.django.DjangoTemplates", 64 | "DIRS": [], 65 | "APP_DIRS": True, 66 | "OPTIONS": { 67 | "context_processors": [ 68 | "django.template.context_processors.debug", 69 | "django.template.context_processors.request", 70 | "django.contrib.auth.context_processors.auth", 71 | "django.contrib.messages.context_processors.messages", 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = "nft_market.wsgi.application" 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 81 | 82 | DATABASES = { 83 | "default": { 84 | "ENGINE": "django.db.backends.sqlite3", 85 | "NAME": BASE_DIR / "db.sqlite3", 86 | } 87 | } 88 | 89 | DATABASE_URL = os.environ.get("DATABASE_URL") 90 | if DATABASE_URL: 91 | import dj_database_url 92 | 93 | DATABASES["default"] = dj_database_url.parse(DATABASE_URL) 94 | if "mysql" in DATABASE_URL: 95 | DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"} 96 | DATABASES["default"]["TEST"] = { 97 | "CHARSET": "utf8mb4", 98 | "COLLATION": "utf8mb4_unicode_ci", 99 | } 100 | 101 | # Password validation 102 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 103 | 104 | AUTH_PASSWORD_VALIDATORS = [ 105 | { 106 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 107 | }, 108 | { 109 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 110 | }, 111 | { 112 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 113 | }, 114 | { 115 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 116 | }, 117 | ] 118 | 119 | # Internationalization 120 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 121 | 122 | LANGUAGE_CODE = "en-us" 123 | 124 | TIME_ZONE = "UTC" 125 | 126 | USE_I18N = True 127 | 128 | USE_L10N = True 129 | 130 | USE_TZ = True 131 | 132 | # Static files (CSS, JavaScript, Images) 133 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 134 | 135 | STATIC_URL = "/static/" 136 | 137 | PURESTAKE_API_KEY = os.environ.get("PURESTAKE_API_KEY") 138 | PURESTAKE_ALGOD_URL = "https://testnet-algorand.api.purestake.io/ps2" 139 | PURESTAKE_INDEXER_URL = "https://testnet-algorand.api.purestake.io/idx2" 140 | 141 | CORS_ALLOWED_ORIGINS = [ 142 | "https://nft-market-azure.vercel.app", 143 | "http://localhost:8080", 144 | "http://127.0.0.1:8080", 145 | ] 146 | 147 | CORS_ALLOWED_ORIGIN_REGEXES = [ 148 | r"^https:\/\/.+\.ulam\.io$", 149 | r"^https:\/\/.+\.ulam\.pro$", 150 | ] 151 | 152 | CSRF_TRUSTED_ORIGINS = [ 153 | "*.ulam.io", 154 | "*.ulam.pro", 155 | "https://nft-market-azure.vercel.app", 156 | "http://localhost:8080", 157 | "http://127.0.0.1:8080", 158 | ] 159 | 160 | REST_FRAMEWORK = { 161 | "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), 162 | "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", 163 | "PAGE_SIZE": 100, 164 | } 165 | 166 | CORS_ALLOW_HEADERS = [ 167 | "accept", 168 | "accept-encoding", 169 | "authorization", 170 | "content-type", 171 | "dnt", 172 | "origin", 173 | "user-agent", 174 | "x-csrftoken", 175 | "x-requested-with", 176 | "x-view-as", 177 | "access-control-allow-origin", 178 | ] 179 | 180 | MEDIA_URL = "/media/" 181 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 182 | STATIC_ROOT = os.path.join(BASE_DIR, "static") 183 | 184 | DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" 185 | GS_BUCKET_NAME = os.environ.get("GS_BUCKET_NAME", "nft-market-staging-media") 186 | 187 | LOGGING = { 188 | "version": 1, 189 | "disable_existing_loggers": False, 190 | "handlers": { 191 | "console": { 192 | "level": "INFO", 193 | "filters": None, 194 | "class": "logging.StreamHandler", 195 | }, 196 | }, 197 | "loggers": { 198 | "django": { 199 | "handlers": ["console"], 200 | "level": "INFO", 201 | }, 202 | }, 203 | } 204 | 205 | CELERY_BROKER_URL = os.getenv( 206 | "CELERY_CONFIG_MODULE", "amqp://guest:guest@nft-market-rabbit:5672//" 207 | ) 208 | -------------------------------------------------------------------------------- /backend/nft_market/settings_dev.py: -------------------------------------------------------------------------------- 1 | from nft_market.settings import * 2 | 3 | 4 | USE_TESTNET = os.environ.get("USE_TESTNET", "true") != "true" 5 | SECRET_KEY = "#4vw_a2_1w&*=_(*3wm(3=650hi_!m#zxc_r4f4c$texam7rk+" 6 | DEBUG = True 7 | DATABASES = { 8 | "default": { 9 | "ENGINE": "django.db.backends.sqlite3", 10 | "NAME": BASE_DIR / "db.sqlite3", 11 | } 12 | } 13 | PURESTAKE_API_KEY = os.environ.get("PURESTAKE_API_KEY") 14 | CORS_ALLOWED_ORIGINS = [ 15 | "http://localhost:8080", 16 | "http://127.0.0.1:8080", 17 | ] 18 | CSRF_TRUSTED_ORIGINS = [ 19 | "http://localhost:8080", 20 | "http://127.0.0.1:8080", 21 | ] 22 | CELERY_BROKER_URL = os.getenv( 23 | "CELERY_CONFIG_MODULE", "amqp://guest:guest@localhost:5672//" 24 | ) 25 | -------------------------------------------------------------------------------- /backend/nft_market/settings_test.py: -------------------------------------------------------------------------------- 1 | from nft_market.settings import * 2 | 3 | 4 | USE_TESTNET = True 5 | SECRET_KEY = "#4vw_a2_1w&*=_(*3wm(3=650hi_!m#zxc_r4f4c$texam7rk+" 6 | DEBUG = False 7 | WSGI_APPLICATION = "nft_market.wsgi.application" 8 | DATABASES = { 9 | "default": { 10 | "ENGINE": "django.db.backends.sqlite3", 11 | "NAME": BASE_DIR / "db.sqlite3", 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/nft_market/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.http import HttpResponse 3 | from django.urls import path, include 4 | from django.conf import settings 5 | 6 | urlpatterns = [ 7 | path("admin/", admin.site.urls), 8 | path("api/", include("nft_market.api.urls")), 9 | path("health/", lambda request: HttpResponse(status=200)), 10 | ] 11 | 12 | if settings.DEBUG: 13 | from django.conf.urls.static import static 14 | 15 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 16 | -------------------------------------------------------------------------------- /backend/nft_market/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulamlabs/OpenNFT/fa4c2db63794a0c75a7e7eb7f63010baeeeee799/backend/nft_market/utils/__init__.py -------------------------------------------------------------------------------- /backend/nft_market/utils/authorization.py: -------------------------------------------------------------------------------- 1 | from nft_market.api.models import User 2 | 3 | 4 | def is_authorized(address): 5 | if User.objects.filter(address=address, is_staff=True).first(): 6 | return True 7 | return False 8 | -------------------------------------------------------------------------------- /backend/nft_market/utils/constants.py: -------------------------------------------------------------------------------- 1 | SET_PRICE = "S" 2 | BID = "B" 3 | CONFIGURE = "C" 4 | BUY_NOW = "BN" 5 | SELL_NOW = "SN" 6 | 7 | CREATOR = "C" 8 | NFT_ID = "N" 9 | ASK_PRICE = "A" 10 | BID_PRICE = "B" 11 | OWNER = "O" 12 | -------------------------------------------------------------------------------- /backend/nft_market/utils/deployment.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from algosdk import encoding 4 | from nft_market.utils.constants import CONFIGURE, NFT_ID, CREATOR 5 | from nft_market.utils.transactions import decode_state 6 | from rest_framework import serializers 7 | 8 | from nft_market.services import algorand 9 | from nft_market.utils.authorization import is_authorized 10 | 11 | 12 | class AppConfiguration: 13 | @staticmethod 14 | def find_and_validate(app_id, tx_id): 15 | found_txs = algorand.indexer.search_transactions( 16 | application_id=app_id, 17 | txid=tx_id, 18 | ) 19 | length = len(found_txs["transactions"]) 20 | if length < 1: 21 | raise serializers.ValidationError( 22 | {"message": "Transaction not found", "retry": True}, 23 | ) 24 | tx = found_txs["transactions"][length - 1] 25 | if "application-transaction" not in tx: 26 | raise serializers.ValidationError({"message": "Invalid transaction type"}) 27 | app_tx = tx["application-transaction"] 28 | correct_args = [base64.b64encode(CONFIGURE.encode("utf-8")).decode("utf-8")] 29 | if app_tx["application-args"] != correct_args: 30 | raise serializers.ValidationError({"message": "Invalid transaction"}) 31 | if not is_authorized(tx["sender"]): 32 | raise serializers.ValidationError( 33 | {"message": "Unauthorized"}, 34 | ) 35 | return app_tx 36 | 37 | 38 | class AssetCreation: 39 | @staticmethod 40 | def find_and_validate(asset_id): 41 | found_txs = algorand.indexer.search_asset_transactions( 42 | asset_id=asset_id, 43 | txn_type="acfg", 44 | ) 45 | length = len(found_txs["transactions"]) 46 | if length < 1: 47 | raise serializers.ValidationError( 48 | {"message": "Transaction not found", "retry": True}, 49 | ) 50 | tx = found_txs["transactions"][length - 1] 51 | if "asset-config-transaction" not in tx: 52 | raise serializers.ValidationError( 53 | {"message": "Invalid transaction type"}, 54 | ) 55 | asset_config = tx["asset-config-transaction"]["params"] 56 | if not is_authorized(asset_config["creator"]): 57 | raise serializers.ValidationError( 58 | {"message": "Unauthorized"}, 59 | ) 60 | return asset_config 61 | 62 | 63 | class ContractDeployment: 64 | @classmethod 65 | def find_and_validate(cls, tx_id, app_id): 66 | payment_tx = cls._find_and_validate_payment_tx(tx_id=tx_id) 67 | app_tx, nft_id, creator_addr = cls._find_and_validate_app_creation_tx( 68 | app_id=app_id 69 | ) 70 | if payment_tx["sender"] != creator_addr: 71 | raise serializers.ValidationError({"message": "Unauthorized"}) 72 | return nft_id 73 | 74 | @staticmethod 75 | def _find_and_validate_payment_tx(tx_id): 76 | found_payment_txs = algorand.indexer.search_transactions(txid=tx_id) 77 | length = len(found_payment_txs["transactions"]) 78 | if length < 1: 79 | raise serializers.ValidationError( 80 | {"message": "Transaction not found", "retry": True}, 81 | ) 82 | tx = found_payment_txs["transactions"][length - 1] 83 | if "payment-transaction" not in tx: 84 | raise serializers.ValidationError({"message": "Invalid transaction type"}) 85 | 86 | return tx 87 | 88 | @staticmethod 89 | def _find_and_validate_app_creation_tx(app_id): 90 | found_app_txs = algorand.indexer.search_transactions(application_id=app_id) 91 | length = len(found_app_txs["transactions"]) 92 | if length < 1: 93 | raise serializers.ValidationError( 94 | {"message": "Transaction not found", "retry": True}, 95 | ) 96 | tx = found_app_txs["transactions"][length - 1] 97 | if "application-transaction" not in tx: 98 | raise serializers.ValidationError({"message": "Invalid transaction type"}) 99 | app_tx = tx["application-transaction"] 100 | global_state_delta = decode_state(tx["global-state-delta"]) 101 | nft_id = global_state_delta[NFT_ID] 102 | creator_addr = encoding.encode_address(global_state_delta[CREATOR]) 103 | 104 | if not is_authorized(creator_addr): 105 | raise serializers.ValidationError({"message": "Unauthorized"}) 106 | return app_tx, nft_id, creator_addr 107 | -------------------------------------------------------------------------------- /backend/nft_market/utils/transactions.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | 4 | def decode_state(state): 5 | decoded_state = {} 6 | for obj in state: 7 | key = base64.b64decode(obj["key"]).decode("utf-8") 8 | type = obj["value"]["action"] 9 | if type == 2: 10 | decoded_state[key] = int(obj["value"]["uint"]) 11 | elif type == 1: 12 | decoded_state[key] = base64.b64decode(obj["value"]["bytes"]) 13 | return decoded_state 14 | 15 | 16 | def decode_global_state(state): 17 | decoded_state = {} 18 | for obj in state: 19 | key = base64.b64decode(obj["key"]).decode("utf-8") 20 | value_type = obj["value"]["type"] 21 | if value_type == 2: 22 | decoded_state[key] = int(obj["value"]["uint"]) 23 | elif value_type == 1: 24 | decoded_state[key] = base64.b64decode(obj["value"]["bytes"]) 25 | return decoded_state 26 | 27 | 28 | def is_non_zero_asset_tx(tx): 29 | asset_tx = tx["asset-transfer-transaction"] 30 | amount = asset_tx["amount"] 31 | return amount > 0 32 | -------------------------------------------------------------------------------- /backend/nft_market/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for nft_market project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nft_market.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nft_market" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Sebastian Gula "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | Django = "3.1.7" 10 | djangorestframework = "3.12.2" 11 | pyteal = {git = "https://github.com/algorand/pyteal.git", rev = "77b2e4452e443d449474d4dee1776fab3eb66f66"} 12 | PyYAML = "^5.4.1" 13 | py-algorand-sdk = "^1.4.1" 14 | django-cors-headers = "^3.7.0" 15 | Pillow = "^8.1.2" 16 | django-filter = "^2.4.0" 17 | celery = "^5.0.5" 18 | uWSGI = "^2.0.19" 19 | dj-database-url = "^0.5.0" 20 | django-storages = {extras = ["google"], version = "^1.11.1"} 21 | psycopg2-binary = "^2.8.6" 22 | requests = "^2.25.1" 23 | 24 | [tool.poetry.dev-dependencies] 25 | black = "^20.8b1" 26 | pytest-django = "^4.2.0" 27 | 28 | [build-system] 29 | requires = ["poetry-core>=1.0.0"] 30 | build-backend = "poetry.core.masonry.api" 31 | -------------------------------------------------------------------------------- /backend/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = nft_market.settings_test 3 | python_files = tests.py test_*.py *_tests.py 4 | -------------------------------------------------------------------------------- /backend/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rest_framework.test import APIClient 3 | 4 | 5 | @pytest.fixture 6 | def api_client(): 7 | return APIClient() 8 | -------------------------------------------------------------------------------- /backend/tests/test_compile.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from rest_framework.exceptions import ErrorDetail 4 | from rest_framework.reverse import reverse 5 | 6 | 7 | class TestCompileEscrow: 8 | def test_invalid_params(self, api_client): 9 | url = reverse("contract-compile-escrow") 10 | response = api_client.post(url) 11 | assert response.status_code == 400 12 | assert response.data == { 13 | "app_id": [ErrorDetail(string="This field is required.", code="required")], 14 | "usdc_id": [ErrorDetail(string="This field is required.", code="required")], 15 | "nft_id": [ErrorDetail(string="This field is required.", code="required")], 16 | } 17 | 18 | def test_correct_params(self, api_client): 19 | with patch("nft_market.api.views.algorand.algod.compile") as mock_method: 20 | mock_method.return_value = {"result": "abc"} 21 | url = reverse("contract-compile-escrow") 22 | response = api_client.post( 23 | url, 24 | { 25 | "app_id": 123, 26 | "usdc_id": 123, 27 | "nft_id": 123, 28 | }, 29 | format="json", 30 | ) 31 | mock_method.assert_called_once() 32 | assert response.status_code == 200 33 | assert response.data == {"result": "abc"} 34 | 35 | 36 | class TestCompileProxy: 37 | def test_invalid_params(self, api_client): 38 | url = reverse("contract-compile-proxy") 39 | response = api_client.post(url) 40 | assert response.status_code == 400 41 | assert response.data == { 42 | "proxy_id": [ 43 | ErrorDetail(string="This field is required.", code="required") 44 | ], 45 | } 46 | 47 | def test_correct_params(self, api_client): 48 | with patch("nft_market.api.views.algorand.algod.compile") as mock_method: 49 | mock_method.return_value = {"result": "abc"} 50 | url = reverse("contract-compile-proxy") 51 | response = api_client.post( 52 | url, 53 | { 54 | "proxy_id": 123, 55 | }, 56 | format="json", 57 | ) 58 | mock_method.assert_called_once() 59 | assert response.status_code == 200 60 | assert response.data == {"result": "abc"} 61 | 62 | 63 | class TestCompileManager: 64 | def test_correct(self, api_client): 65 | with patch("nft_market.api.views.algorand.algod.compile") as mock_method: 66 | mock_method.return_value = {"result": "abc"} 67 | url = reverse("contract-compile-manager") 68 | response = api_client.post(url) 69 | assert response.status_code == 200 70 | assert response.data == { 71 | "result": "abc", 72 | "params": { 73 | "num_local_ints": 1, 74 | "num_local_byte_slices": 0, 75 | "num_global_ints": 4, 76 | "num_global_byte_slices": 3, 77 | }, 78 | } 79 | 80 | 81 | class TestCompileClear: 82 | def test_correct(self, api_client): 83 | with patch("nft_market.api.views.algorand.algod.compile") as mock_method: 84 | mock_method.return_value = {"result": "abc"} 85 | url = reverse("contract-compile-clear") 86 | response = api_client.post(url) 87 | assert response.status_code == 200 88 | assert response.data == { 89 | "result": "abc", 90 | } 91 | -------------------------------------------------------------------------------- /contracts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 12, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single", 23 | { 24 | "avoidEscape": true 25 | } 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ], 31 | "object-curly-spacing": [ 32 | "error", 33 | "always" 34 | ], 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/algob.config.js: -------------------------------------------------------------------------------- 1 | // NOTE: below we provide some example accounts. 2 | // DON'T this account in any working environment because everyone can check it and use 3 | // the private keys (this accounts are visible to everyone). 4 | 5 | // NOTE: to be able to execute transactions, you need to use an active account with 6 | // a sufficient ALGO balance. 7 | 8 | /** 9 | Check our /docs/algob-config.md documentation (https://github.com/scale-it/algorand-builder/blob/master/docs/algob-config.md) for more configuration options and ways how to 10 | load a private keys: 11 | + using mnemonic 12 | + using binary secret key 13 | + using KMD daemon 14 | + loading from a file 15 | + loading from an environment variable 16 | + ... 17 | */ 18 | 19 | // ## ACCOUNTS USING mnemonic ## 20 | const { mkAccounts, algodCredentialsFromEnv } = require("@algorand-builder/algob"); 21 | let accounts = mkAccounts([{ 22 | // This account is created using `make setup-master-account` command from our 23 | // `/infrastructure` directory. It already has many ALGOs 24 | name: "master", 25 | addr: "DXWPYQDURUNRP3CLC7NDEQXEYLZCC5EQ6YWFJ5Q5IKLB5FA35ADB4326TE", 26 | mnemonic: "adult parent couple era recipe used quarter glance spray convince calm debate settle budget shoulder sheriff segment room divert art boss actor fat able chat" 27 | }]); 28 | 29 | // ## ACCOUNTS loaded from a FILE ## 30 | // const { loadAccountsFromFileSync } = require("@algorand-builder/algob"); 31 | // const accFromFile = loadAccountsFromFileSync("assets/accounts_generated.yaml"); 32 | // accounts = accounts.concat(accFromFile); 33 | 34 | 35 | 36 | /// ## Enabling KMD access 37 | /// Please check https://github.com/scale-it/algorand-builder/blob/master/docs/algob-config.md#credentials for more details and more methods. 38 | 39 | // process.env.$KMD_DATA = "/path_to/KMD_DATA"; 40 | // let kmdCred = KMDCredentialsFromEnv(); 41 | 42 | 43 | 44 | // ## Algod Credentials 45 | // You can set the credentials directly in this file: 46 | 47 | let defaultCfg = { 48 | host: "http://127.0.0.1", 49 | port: 8777, 50 | // Below is a token created through our script in `/infrastructure` 51 | // If you use other setup, update it accordignly (eg content of algorand-node-data/algod.token) 52 | token: "a136e80ac4e1f54add873633eb786f740f19ba839c6c5a59ee6a36744d191583", 53 | accounts: accounts, 54 | // if you want to load accounts from KMD, you need to add the kmdCfg object. Please read 55 | // algob-config.md documentation for details. 56 | // kmdCfg: kmdCfg, 57 | }; 58 | 59 | // You can also use Environment variables to get Algod credentials 60 | // Please check https://github.com/scale-it/algorand-builder/blob/master/docs/algob-config.md#credentials for more details and more methods. 61 | // Method 1 62 | process.env.ALGOD_ADDR = "127.0.0.1:8080"; 63 | process.env.ALGOD_TOKEN = "algod_token"; 64 | let algodCred = algodCredentialsFromEnv(); 65 | 66 | 67 | let envCfg = { 68 | host: algodCred.host, 69 | port: algodCred.port, 70 | token: algodCred.token, 71 | accounts: accounts 72 | } 73 | 74 | 75 | module.exports = { 76 | networks: { 77 | default: defaultCfg, 78 | prod: envCfg 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /contracts/assets: -------------------------------------------------------------------------------- 1 | ../backend/nft_market/contracts/assets/ -------------------------------------------------------------------------------- /contracts/common/constants.js: -------------------------------------------------------------------------------- 1 | const ESCROW_ADDRESS = 'E'; 2 | const ASK_PRICE = 'A'; 3 | const BIDS_AMOUNT = 'B'; 4 | const OWNER_ADDRESS = 'O'; 5 | const CREATOR_ADDRESS = 'C'; 6 | 7 | const BID_PRICE = 'B'; 8 | 9 | const BID = 'B'; 10 | const SET_PRICE = 'S'; 11 | const BUY_NOW = 'BN'; 12 | const SELL_NOW = 'SN'; 13 | const CONFIGURE = 'C'; 14 | 15 | // eslint-disable-next-line no-undef 16 | module.exports = { 17 | ESCROW_ADDRESS, 18 | ASK_PRICE, 19 | BIDS_AMOUNT, 20 | OWNER_ADDRESS, 21 | CREATOR_ADDRESS, 22 | BID_PRICE, 23 | BID, 24 | SET_PRICE, 25 | BUY_NOW, 26 | SELL_NOW, 27 | CONFIGURE 28 | }; 29 | -------------------------------------------------------------------------------- /contracts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-project", 3 | "version": "0.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "sample-project", 9 | "version": "0.0.1", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "chai": "^4.3.4" 13 | } 14 | }, 15 | "node_modules/assertion-error": { 16 | "version": "1.1.0", 17 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 18 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 19 | "dev": true, 20 | "engines": { 21 | "node": "*" 22 | } 23 | }, 24 | "node_modules/chai": { 25 | "version": "4.3.4", 26 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", 27 | "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", 28 | "dev": true, 29 | "dependencies": { 30 | "assertion-error": "^1.1.0", 31 | "check-error": "^1.0.2", 32 | "deep-eql": "^3.0.1", 33 | "get-func-name": "^2.0.0", 34 | "pathval": "^1.1.1", 35 | "type-detect": "^4.0.5" 36 | }, 37 | "engines": { 38 | "node": ">=4" 39 | } 40 | }, 41 | "node_modules/check-error": { 42 | "version": "1.0.2", 43 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 44 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 45 | "dev": true, 46 | "engines": { 47 | "node": "*" 48 | } 49 | }, 50 | "node_modules/deep-eql": { 51 | "version": "3.0.1", 52 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 53 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 54 | "dev": true, 55 | "dependencies": { 56 | "type-detect": "^4.0.0" 57 | }, 58 | "engines": { 59 | "node": ">=0.12" 60 | } 61 | }, 62 | "node_modules/get-func-name": { 63 | "version": "2.0.0", 64 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 65 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 66 | "dev": true, 67 | "engines": { 68 | "node": "*" 69 | } 70 | }, 71 | "node_modules/pathval": { 72 | "version": "1.1.1", 73 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 74 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 75 | "dev": true, 76 | "engines": { 77 | "node": "*" 78 | } 79 | }, 80 | "node_modules/type-detect": { 81 | "version": "4.0.8", 82 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 83 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 84 | "dev": true, 85 | "engines": { 86 | "node": ">=4" 87 | } 88 | } 89 | }, 90 | "dependencies": { 91 | "assertion-error": { 92 | "version": "1.1.0", 93 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 94 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 95 | "dev": true 96 | }, 97 | "chai": { 98 | "version": "4.3.4", 99 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", 100 | "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", 101 | "dev": true, 102 | "requires": { 103 | "assertion-error": "^1.1.0", 104 | "check-error": "^1.0.2", 105 | "deep-eql": "^3.0.1", 106 | "get-func-name": "^2.0.0", 107 | "pathval": "^1.1.1", 108 | "type-detect": "^4.0.5" 109 | } 110 | }, 111 | "check-error": { 112 | "version": "1.0.2", 113 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 114 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 115 | "dev": true 116 | }, 117 | "deep-eql": { 118 | "version": "3.0.1", 119 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 120 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 121 | "dev": true, 122 | "requires": { 123 | "type-detect": "^4.0.0" 124 | } 125 | }, 126 | "get-func-name": { 127 | "version": "2.0.0", 128 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 129 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 130 | "dev": true 131 | }, 132 | "pathval": { 133 | "version": "1.1.1", 134 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 135 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 136 | "dev": true 137 | }, 138 | "type-detect": { 139 | "version": "4.0.8", 140 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 141 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 142 | "dev": true 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nft-market", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "chai": "^4.3.4", 8 | "eslint": "^7.19.0" 9 | }, 10 | "scripts": { 11 | "algob": "algob", 12 | "lint": "eslint --ext .js,.ts,.mjs test", 13 | "lint:fix": "eslint --fix --ext .js,.ts,.mjs test", 14 | "test": "mocha", 15 | "build": "echo ok" 16 | }, 17 | "dependencies": { 18 | "algosdk": "^1.8.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "pyteal" 3 | version = "0.6.2" 4 | description = "Algorand Smart Contracts in Python" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | develop = false 9 | 10 | [package.source] 11 | type = "git" 12 | url = "https://github.com/algorand/pyteal.git" 13 | reference = "77b2e4452e443d449474d4dee1776fab3eb66f66" 14 | resolved_reference = "77b2e4452e443d449474d4dee1776fab3eb66f66" 15 | 16 | [[package]] 17 | name = "pyyaml" 18 | version = "5.4.1" 19 | description = "YAML parser and emitter for Python" 20 | category = "main" 21 | optional = false 22 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 23 | 24 | [metadata] 25 | lock-version = "1.1" 26 | python-versions = "^3.7" 27 | content-hash = "44bd1e2db76383d2a87da9935c2782fbde34183951bcde991ab8a2ee166ba340" 28 | 29 | [metadata.files] 30 | pyteal = [] 31 | pyyaml = [ 32 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 33 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 34 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 35 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 36 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 37 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 38 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 39 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 40 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 41 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 42 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 43 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 44 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 45 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 46 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 47 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 48 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 49 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 50 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 51 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 52 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 53 | ] 54 | -------------------------------------------------------------------------------- /contracts/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nft-market" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["sebastiangula "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.7" 9 | pyteal = {git = "https://github.com/algorand/pyteal.git", rev = "77b2e4452e443d449474d4dee1776fab3eb66f66"} 10 | PyYAML = "^5.4.1" 11 | 12 | [tool.poetry.dev-dependencies] 13 | 14 | [build-system] 15 | requires = ["poetry>=0.12"] 16 | build-backend = "poetry.masonry.api" 17 | -------------------------------------------------------------------------------- /contracts/scripts/deploy.js: -------------------------------------------------------------------------------- 1 | // Original file: https://github.com/scale-it/algorand-builder/blob/master/examples/crowdfunding/scripts/createApp.js 2 | /* globals module, require */ 3 | 4 | const { CONFIGURE } = require('../common/constants.js'); 5 | 6 | const { stringToBytes } = require('@algorand-builder/algob'); 7 | const { executeTransaction } = require('@algorand-builder/algob'); 8 | const { TransactionType, SignType } = require('@algorand-builder/runtime/build/types.js'); 9 | 10 | const NFT_ASSET_ID = 14001707; 11 | const USDC_ASSET_ID = 14098899; 12 | 13 | async function run (runtimeEnv, deployer) { 14 | const masterAccount = deployer.accountsByName.get('master'); 15 | 16 | // Initialize app arguments 17 | let appArgs = [`int:${USDC_ASSET_ID}`, `int:${NFT_ASSET_ID}`]; 18 | 19 | // Create Application 20 | // Note: An Account can have maximum of 10 Applications. 21 | await deployer.ensureCompiled('manager.py', true, {}); 22 | const res = await deployer.deploySSC( 23 | 'manager.py', // approval program 24 | 'clear.py', // clear program 25 | { 26 | sender: masterAccount, 27 | localInts: 1, 28 | localBytes: 0, 29 | globalInts: 4, 30 | globalBytes: 3, 31 | appArgs: appArgs, 32 | accounts: [masterAccount.addr] 33 | }, 34 | {} 35 | ); 36 | const applicationID = res.appID; 37 | 38 | // Get escrow account address 39 | const escrowAccount = await deployer.loadLogic('escrow.py', [], { 40 | app_id: applicationID, 41 | usdc_id: USDC_ASSET_ID, 42 | nft_id: NFT_ASSET_ID 43 | }); 44 | console.log('Escrow Account Address:', escrowAccount.address()); 45 | 46 | // Send funds for minimum escrow balance 47 | const algoTxnParams = { 48 | type: TransactionType.TransferAlgo, 49 | sign: SignType.SecretKey, 50 | fromAccount: masterAccount, 51 | toAccountAddr: escrowAccount.address(), 52 | amountMicroAlgos: 302000, 53 | payFlags: {} 54 | }; 55 | await executeTransaction(deployer, algoTxnParams); 56 | 57 | console.log('Opting-In For Escrow'); 58 | let txnParams = [ 59 | { 60 | type: TransactionType.CallNoOpSSC, 61 | sign: SignType.SecretKey, 62 | fromAccount: masterAccount, 63 | appId: applicationID, 64 | appArgs: [stringToBytes(CONFIGURE)], 65 | accounts: [escrowAccount.address()], 66 | payFlags: { totalFee: 1000 } 67 | }, 68 | { 69 | type: TransactionType.TransferAsset, 70 | sign: SignType.LogicSignature, 71 | fromAccount: { addr: escrowAccount.address() }, 72 | toAccountAddr: escrowAccount.address(), 73 | lsig: escrowAccount, 74 | amount: 0, 75 | assetID: NFT_ASSET_ID, 76 | payFlags: { totalFee: 1000 } 77 | }, 78 | { 79 | type: TransactionType.TransferAsset, 80 | sign: SignType.LogicSignature, 81 | fromAccount: { addr: escrowAccount.address() }, 82 | toAccountAddr: escrowAccount.address(), 83 | lsig: escrowAccount, 84 | amount: 0, 85 | assetID: USDC_ASSET_ID, 86 | payFlags: { totalFee: 1000 } 87 | } 88 | ]; 89 | await executeTransaction(deployer, txnParams); 90 | console.log('Application Is Ready'); 91 | } 92 | 93 | module.exports = { default: run }; 94 | -------------------------------------------------------------------------------- /contracts/test/utils/assets.mjs: -------------------------------------------------------------------------------- 1 | import { SignType, TransactionType } from '@algorand-builder/runtime/build/types.js'; 2 | 3 | export function setupAssets(runtime, account) { 4 | return { 5 | NFTAssetId: setupNFTAsset(runtime, account), 6 | USDCAssetId: setupUSDCAsset(runtime, account), 7 | }; 8 | } 9 | 10 | function setupNFTAsset(runtime, account) { 11 | account.addAsset(111, 'NFT', { 12 | creator: 'addr-1', 13 | total: 10000000, 14 | decimals: 10, 15 | defaultFrozen: false, 16 | unitName: 'ASSET', 17 | name: 'ASSET', 18 | url: 'assetUrl', 19 | metadataHash: 'hash', 20 | manager: 'addr-1', 21 | reserve: 'addr-2', 22 | freeze: 'addr-3', 23 | clawback: 'addr-4' 24 | }); 25 | runtime.store.assetDefs.set(111, account.address); 26 | return 111; 27 | } 28 | 29 | function setupUSDCAsset(runtime, account) { 30 | account.addAsset(123, 'USDC', { 31 | creator: 'addr-1', 32 | total: 10000000, 33 | decimals: 10, 34 | defaultFrozen: false, 35 | unitName: 'ASSET', 36 | name: 'ASSET', 37 | url: 'assetUrl', 38 | metadataHash: 'hash', 39 | manager: 'addr-1', 40 | reserve: 'addr-2', 41 | freeze: 'addr-3', 42 | clawback: 'addr-4' 43 | }); 44 | runtime.store.assetDefs.set(123, account.address); 45 | return 123; 46 | } 47 | 48 | export function fundAccounts(runtime, fundingAccount, accounts, assets) { 49 | function fund(assetId, account) { 50 | runtime.optIntoASA(assetId, account.address, {}); 51 | let tx = [ 52 | { 53 | type: TransactionType.TransferAsset, 54 | assetID: assetId, 55 | sign: SignType.SecretKey, 56 | fromAccount: fundingAccount.account, 57 | toAccountAddr: account.address, 58 | amount: 1000000, 59 | payFlags: { 60 | totalFee: 1000 61 | } 62 | } 63 | ]; 64 | runtime.executeTx(tx); 65 | } 66 | accounts.forEach((account) => { 67 | Object.keys(assets).forEach((key) => { 68 | fund(assets[key], account); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /contracts/test/utils/errors.mjs: -------------------------------------------------------------------------------- 1 | // Original file: https://github.com/scale-it/algorand-builder/blob/master/packages/runtime/test/helpers/errors.ts 2 | 3 | import { RuntimeError } from '@algorand-builder/runtime/build/errors/runtime-errors.js'; 4 | import chai from 'chai'; 5 | 6 | const { assert, AssertionError } = chai; 7 | 8 | // Takes string array and executes opcode to expect teal error 9 | export function execExpectError(stack, strs, op, err) { 10 | return () => { 11 | for (const s of strs) { 12 | stack.push(s); 13 | } 14 | expectTealError(() => op.execute(stack), err); 15 | }; 16 | } 17 | 18 | export function expectTealError(f, errorDescriptor, matchMessage = undefined, errorMessage = undefined) { 19 | try { 20 | f(); 21 | } catch (error) { 22 | assert.instanceOf(error, RuntimeError, errorMessage); 23 | assert.equal(error.number, errorDescriptor.number, errorMessage); 24 | assert.notMatch( 25 | error.message, 26 | /%[a-zA-Z][a-zA-Z0-9]*%/, 27 | 'TealError has an non-replaced variable tag' 28 | ); 29 | 30 | if (typeof matchMessage === 'string') { 31 | assert.include(error.message, matchMessage, errorMessage); 32 | } else if (matchMessage !== undefined) { 33 | assert.match(error.message, matchMessage, errorMessage); 34 | } 35 | 36 | return; 37 | } 38 | throw new AssertionError( 39 | `TealError number ${errorDescriptor.number} expected, but no Error was thrown` 40 | ); 41 | } 42 | 43 | export async function expectTealErrorAsync(f, errorDescriptor, matchMessage = undefined) { 44 | // We create the error here to capture the stack trace before the await. 45 | // This makes things easier, at least as long as we don't have async stack 46 | // traces. This may change in the near-ish future. 47 | const error = new AssertionError( 48 | `TealError number ${errorDescriptor.number} expected, but no Error was thrown` 49 | ); 50 | 51 | const match = String(matchMessage); 52 | const notExactMatch = new AssertionError( 53 | `TealError was correct, but should have include "${match}" but got "` 54 | ); 55 | 56 | const notRegexpMatch = new AssertionError( 57 | `TealError was correct, but should have matched regex ${match} but got "` 58 | ); 59 | 60 | try { 61 | await f(); 62 | } catch (error) { 63 | assert.instanceOf(error, RuntimeError); 64 | assert.equal(error.number, errorDescriptor.number); 65 | assert.notMatch( 66 | error.message, 67 | /%[a-zA-Z][a-zA-Z0-9]*%/, 68 | 'TealError has an non-replaced variable tag' 69 | ); 70 | 71 | if (matchMessage !== undefined) { 72 | if (typeof matchMessage === 'string') { 73 | if (!error.message.includes(matchMessage)) { 74 | notExactMatch.message += `${String(error.message)}`; 75 | throw notExactMatch; 76 | } 77 | } else { 78 | if (matchMessage.exec(error.message) === null) { 79 | notRegexpMatch.message += `${String(error.message)}`; 80 | throw notRegexpMatch; 81 | } 82 | } 83 | } 84 | 85 | return; 86 | } 87 | 88 | throw error; 89 | } 90 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # nft-market 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nft-market", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@randlabs/myalgo-connect": "^1.0.1", 12 | "@tailwindcss/aspect-ratio": "^0.2.0", 13 | "@tailwindcss/forms": "^0.2.1", 14 | "algosdk": "^1.8.1", 15 | "autoprefixer": "9", 16 | "axios": "^0.21.1", 17 | "buffer": "^6.0.3", 18 | "core-js": "^3.6.5", 19 | "humanize-string": "^2.1.0", 20 | "lodash": "^4.17.20", 21 | "moment": "^2.29.1", 22 | "tailwindcss": "^2.0.2", 23 | "uuidv4": "^6.2.6", 24 | "vue": "^2.6.11", 25 | "vue-image-placeholder": "^0.1.1", 26 | "vue-router": "^3.2.0", 27 | "vue-simple-spinner": "^1.2.10", 28 | "vue-tailwind": "^2.1.1", 29 | "vuex": "^3.4.0" 30 | }, 31 | "devDependencies": { 32 | "@vue/cli-plugin-babel": "~4.5.0", 33 | "@vue/cli-plugin-eslint": "~4.5.0", 34 | "@vue/cli-plugin-router": "~4.5.0", 35 | "@vue/cli-plugin-vuex": "~4.5.0", 36 | "@vue/cli-service": "~4.5.0", 37 | "@vue/eslint-config-airbnb": "^5.0.2", 38 | "@vue/eslint-config-prettier": "^6.0.0", 39 | "@vue/eslint-config-standard": "^5.1.2", 40 | "babel-eslint": "^10.1.0", 41 | "eslint": "^6.7.2", 42 | "eslint-plugin-import": "^2.20.2", 43 | "eslint-plugin-node": "^11.1.0", 44 | "eslint-plugin-prettier": "^3.1.3", 45 | "eslint-plugin-promise": "^4.2.1", 46 | "eslint-plugin-standard": "^4.0.0", 47 | "eslint-plugin-vue": "^6.2.2", 48 | "prettier": "^1.19.1", 49 | "vue-template-compiler": "^2.6.11" 50 | }, 51 | "eslintConfig": { 52 | "root": true, 53 | "env": { 54 | "node": true 55 | }, 56 | "extends": [ 57 | "plugin:vue/recommended", 58 | "eslint:recommended" 59 | ], 60 | "parserOptions": { 61 | "parser": "babel-eslint" 62 | }, 63 | "rules": { 64 | "quotes": [ 65 | "warn", 66 | "single", 67 | { 68 | "avoidEscape": true 69 | } 70 | ], 71 | "object-curly-spacing": [ 72 | "warn", 73 | "always" 74 | ], 75 | "indent": [ 76 | "warn", 77 | 2 78 | ], 79 | "semi": [ 80 | "error", 81 | "always" 82 | ], 83 | "no-multiple-empty-lines": [ 84 | "warn" 85 | ] 86 | } 87 | }, 88 | "browserslist": [ 89 | "> 1%", 90 | "last 2 versions", 91 | "not dead" 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // eslint-disable-next-line global-require 3 | plugins: [require('tailwindcss')('tailwind.js'), require('autoprefixer')()] 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulamlabs/OpenNFT/fa4c2db63794a0c75a7e7eb7f63010baeeeee799/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | OpenNFT 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 125 | 136 | -------------------------------------------------------------------------------- /frontend/src/assets/algorand-logo.svg: -------------------------------------------------------------------------------- 1 | algorand-algo-logo -------------------------------------------------------------------------------- /frontend/src/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | @apply text-4xl; 3 | } 4 | 5 | h2 { 6 | @apply text-2xl; 7 | } 8 | 9 | h3 { 10 | @apply text-xl; 11 | } 12 | 13 | h4 { 14 | @apply text-lg; 15 | } 16 | 17 | .body { 18 | min-width: 24rem; 19 | } -------------------------------------------------------------------------------- /frontend/src/assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /frontend/src/components/ActionButton.vue: -------------------------------------------------------------------------------- 1 | 55 | 191 | 201 | -------------------------------------------------------------------------------- /frontend/src/components/AddressLink.vue: -------------------------------------------------------------------------------- 1 | 7 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 27 | 64 | -------------------------------------------------------------------------------- /frontend/src/components/AssetTitle.vue: -------------------------------------------------------------------------------- 1 | 14 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/ConnectToWalletButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 20 | -------------------------------------------------------------------------------- /frontend/src/components/FeaturedItem.vue: -------------------------------------------------------------------------------- 1 | 70 | 164 | 172 | -------------------------------------------------------------------------------- /frontend/src/components/FileInput.vue: -------------------------------------------------------------------------------- 1 | 27 | 63 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 81 | 112 | -------------------------------------------------------------------------------- /frontend/src/components/NInput.vue: -------------------------------------------------------------------------------- 1 | 42 | 92 | -------------------------------------------------------------------------------- /frontend/src/components/NumberInput.vue: -------------------------------------------------------------------------------- 1 | 24 | 96 | -------------------------------------------------------------------------------- /frontend/src/components/PageContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /frontend/src/components/TopImage.vue: -------------------------------------------------------------------------------- 1 | 17 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/TransactionLink.vue: -------------------------------------------------------------------------------- 1 | 7 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/TransactionTable.vue: -------------------------------------------------------------------------------- 1 | 86 | 155 | -------------------------------------------------------------------------------- /frontend/src/components/UpdateScheduler.vue: -------------------------------------------------------------------------------- 1 | 4 | 42 | -------------------------------------------------------------------------------- /frontend/src/components/cards/AssetListCard.vue: -------------------------------------------------------------------------------- 1 | 67 | 140 | 146 | -------------------------------------------------------------------------------- /frontend/src/components/cards/ViewMore.vue: -------------------------------------------------------------------------------- 1 | 13 | 23 | 30 | -------------------------------------------------------------------------------- /frontend/src/components/modals/ActionModal.vue: -------------------------------------------------------------------------------- 1 | 35 | 52 | -------------------------------------------------------------------------------- /frontend/src/components/modals/BuyNowModal.vue: -------------------------------------------------------------------------------- 1 | 61 | 168 | -------------------------------------------------------------------------------- /frontend/src/components/modals/CancelModal.vue: -------------------------------------------------------------------------------- 1 | 35 | 76 | -------------------------------------------------------------------------------- /frontend/src/components/modals/ModalWrapper.vue: -------------------------------------------------------------------------------- 1 | 21 | 31 | 41 | -------------------------------------------------------------------------------- /frontend/src/components/modals/SelectAccountModal.vue: -------------------------------------------------------------------------------- 1 | 64 | 105 | -------------------------------------------------------------------------------- /frontend/src/components/modals/SelectWalletModal.vue: -------------------------------------------------------------------------------- 1 | 38 | 97 | -------------------------------------------------------------------------------- /frontend/src/config/index.js: -------------------------------------------------------------------------------- 1 | export const ALGORAND_LEDGER = 'TestNet'; 2 | export const USDC_ID = 14001707; 3 | export const USDC_DECIMAL_POINTS = 2; 4 | export const ASSET_URL = 'nft-market.ulam.io'; 5 | export var BACKEND_URL; 6 | if (process.env.NODE_ENV === 'development') { 7 | BACKEND_URL = 'http://localhost:8000'; 8 | } else { 9 | BACKEND_URL = 'https://nft-be.ulam.io'; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/mixins/authOnly.js: -------------------------------------------------------------------------------- 1 | import { mapGetters } from 'vuex'; 2 | 3 | export default { 4 | computed: { 5 | ...mapGetters({ 6 | account: 'algorand/account' 7 | }) 8 | }, 9 | watch: { 10 | account() { 11 | this.redirectIfNotLoggedIn(); 12 | } 13 | }, 14 | methods: { 15 | redirectIfNotLoggedIn() { 16 | if (!this.account) { 17 | this.goBack(); 18 | } 19 | }, 20 | }, 21 | mounted() { 22 | this.redirectIfNotLoggedIn(); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/src/mixins/goBack.js: -------------------------------------------------------------------------------- 1 | import { mapGetters } from 'vuex'; 2 | 3 | export default { 4 | computed: { 5 | ...mapGetters({ 6 | isStaff: 'internal/isStaff' 7 | }) 8 | }, 9 | methods: { 10 | goBack() { 11 | this.hasHistory() && this.isStaff ? this.$router.go(-1) : this.$router.push({ 12 | name: 'Home' 13 | }); 14 | }, 15 | hasHistory() { 16 | return window.history.length > 2; 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/mixins/staffOnly.js: -------------------------------------------------------------------------------- 1 | import { mapGetters } from 'vuex'; 2 | 3 | export default { 4 | computed: { 5 | ...mapGetters({ 6 | account: 'algorand/account', 7 | isStaff: 'internal/isStaff', 8 | isFetched: 'internal/isFetched' 9 | }) 10 | }, 11 | watch: { 12 | account() { 13 | this.redirectIfNotStaff(); 14 | }, 15 | isStaff() { 16 | this.redirectIfNotStaff(); 17 | }, 18 | isFetched() { 19 | this.redirectIfNotStaff(); 20 | } 21 | }, 22 | methods: { 23 | redirectIfNotStaff() { 24 | if (!this.account) { 25 | this.goBack(); 26 | } 27 | if (!this.isStaff && this.isFetched) { 28 | this.goBack(); 29 | } 30 | } 31 | }, 32 | mounted() { 33 | this.redirectIfNotStaff(); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import AddAsset from '@/views/AddAsset.vue'; 4 | import AssetList from '@/views/AssetList.vue'; 5 | import DeployContract from '@/views/DeployContract.vue'; 6 | import AssetDetails from '@/views/AssetDetails'; 7 | import UserAssetList from '@/views/UserAssetList'; 8 | import ForSale from '@/views/ForSale'; 9 | import AllItems from '@/views/AllItems'; 10 | import HighestBids from '@/views/HighestBids'; 11 | import RecentlyAdded from '@/views/RecentlyAdded'; 12 | import UserCreatedAssetList from '@/views/UserCreatedAssetList'; 13 | import UserBidList from '@/views/UserBidList'; 14 | 15 | Vue.use(VueRouter); 16 | 17 | const routes = [ 18 | { 19 | path: '/', 20 | name: 'Home', 21 | component: AssetList 22 | }, 23 | { 24 | path: '/nft/:id', 25 | name: 'AssetDetails', 26 | component: AssetDetails 27 | }, 28 | { 29 | path: '/admin/add-nft/:id', 30 | name: 'DeployContract', 31 | component: DeployContract, 32 | meta: true 33 | }, 34 | { 35 | path: '/admin/add-nft', 36 | name: 'AddAsset', 37 | component: AddAsset, 38 | staffOnly: true 39 | }, 40 | { 41 | path: '/admin/nft-list', 42 | name: 'UserCreatedAssetList', 43 | component: UserCreatedAssetList, 44 | staffOnly: true 45 | }, 46 | { 47 | path: '/my-nfts', 48 | name: 'UserAssetList', 49 | component: UserAssetList, 50 | }, 51 | { 52 | path: '/my-bids', 53 | name: 'UserBidList', 54 | component: UserBidList, 55 | }, 56 | { 57 | path: '/all-items', 58 | name: 'AllItems', 59 | component: AllItems, 60 | }, 61 | { 62 | path: '/for-sale', 63 | name: 'ForSale', 64 | component: ForSale, 65 | }, 66 | { 67 | path: '/highest-bids', 68 | name: 'HighestBids', 69 | component: HighestBids, 70 | }, 71 | { 72 | path: '/recently-added', 73 | name: 'RecentlyAdded', 74 | component: RecentlyAdded, 75 | } 76 | ]; 77 | 78 | const router = new VueRouter({ 79 | routes, 80 | scrollBehavior() { 81 | return { x: 0, y: 0 }; 82 | } 83 | }); 84 | 85 | export default router; 86 | -------------------------------------------------------------------------------- /frontend/src/services/algoExplorer.js: -------------------------------------------------------------------------------- 1 | import { emitError } from '@/utils/errors'; 2 | import { InvalidResponse } from '@/services/base'; 3 | import { ALGORAND_LEDGER } from '@/config'; 4 | 5 | export class AlgoExplorer { 6 | constructor(ledger) { 7 | if (ledger.toUpperCase() === 'TESTNET') { 8 | this.url = 'https://api.testnet.algoexplorer.io'; 9 | } else { 10 | this.url = 'https://api.algoexplorer.io'; 11 | } 12 | } 13 | 14 | async get(path) { 15 | try { 16 | const response = await fetch(`${this.url}${path}`); 17 | if (response.status !== 200) { 18 | throw new InvalidResponse(response); 19 | } 20 | return await response.json(); 21 | } catch (e) { 22 | emitError('Could not get information from the Algorand blockchain'); 23 | throw e; 24 | } 25 | } 26 | 27 | async post(path, payload, headers={}) { 28 | try { 29 | const response = await fetch(`${this.url}${path}`, { 30 | method: 'POST', 31 | body: payload, 32 | headers, 33 | }); 34 | if (response.status !== 200) { 35 | throw new InvalidResponse(response); 36 | } 37 | return await response.json(); 38 | } catch (e) { 39 | emitError('Could not broadcast information to the Algorand blockchain'); 40 | throw e; 41 | } 42 | } 43 | } 44 | 45 | export const algoExplorer = new AlgoExplorer(ALGORAND_LEDGER); 46 | -------------------------------------------------------------------------------- /frontend/src/services/algoSignerWallet.js: -------------------------------------------------------------------------------- 1 | import { emitError } from '@/utils/errors'; 2 | import eventBus from '@/utils/eventBus'; 3 | import { TX_FORMAT, unwrapTxs } from '@/utils/transactions'; 4 | import { handleNodeExceptions } from '@/services/base'; 5 | 6 | export class AlgoSignerWallet { 7 | constructor(algoSignerWallet) { 8 | this.algoSignerWallet = algoSignerWallet; 9 | } 10 | 11 | async blockingCall(func, retry = true) { 12 | // Ugly but works 13 | while (this.promise) { 14 | await this.promise; 15 | } 16 | try { 17 | this.promise = func(); 18 | try { 19 | return await this.promise; 20 | } catch (e) { 21 | if (e.message === 'Another query processing' && retry) { 22 | return new Promise(resolve => { 23 | window.setTimeout( 24 | async () => { 25 | resolve(await this.blockingCall(func, false)); 26 | }, 500 27 | ); 28 | }); 29 | } else { 30 | throw e; 31 | } 32 | } 33 | } finally { 34 | this.promise = null; 35 | } 36 | } 37 | 38 | async connect() { 39 | try { 40 | return await this.algoSignerWallet.connect(); 41 | } catch (e) { 42 | emitError('Could not connect to AlgoSignerWallet'); 43 | throw e; 44 | } 45 | } 46 | 47 | // eslint-disable-next-line no-unused-vars 48 | async sign(txs, description=null, separate=true) { 49 | try { 50 | txs = unwrapTxs(txs, TX_FORMAT.Signer); 51 | const signedTxs = []; 52 | let count = 1; 53 | const txsCount = txs.length; 54 | for (const tx of txs) { 55 | if (description) { 56 | if (txsCount > 1) { 57 | eventBus.$emit('set-action-message', `Signing ${count} of ${txs.length} ${description}s...`); 58 | } else { 59 | eventBus.$emit('set-action-message', `Signing ${description}...`); 60 | } 61 | } 62 | signedTxs.push(await this.blockingCall(async () => await this.algoSignerWallet.sign(tx))); 63 | count += 1; 64 | } 65 | return signedTxs; 66 | } catch (e) { 67 | emitError('Transaction could not be signed'); 68 | throw e; 69 | } 70 | } 71 | 72 | async accounts(params) { 73 | try { 74 | return await this.blockingCall(() => this.algoSignerWallet.accounts(params)); 75 | } catch (e) { 76 | emitError('Could not get information about accounts'); 77 | throw e; 78 | } 79 | } 80 | 81 | async algod(params) { 82 | try { 83 | return await this.blockingCall(() => this.algoSignerWallet.algod(params)); 84 | } catch (e) { 85 | emitError('Could not get information from the Algorand blockchain'); 86 | throw e; 87 | } 88 | } 89 | 90 | async send(params, showSucess = true) { 91 | try { 92 | eventBus.$emit('set-action-message', 'Sending...'); 93 | const tx = await this.blockingCall(async () => await this.algoSignerWallet.send({ 94 | ledger: params.ledger, 95 | tx: params.tx 96 | })); 97 | if (showSucess) { 98 | eventBus.$emit('transaction-success', tx.txId); 99 | } 100 | return tx; 101 | } catch (e) { 102 | await handleNodeExceptions(e); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /frontend/src/services/base.js: -------------------------------------------------------------------------------- 1 | import { emitError } from '@/utils/errors'; 2 | 3 | export function InvalidResponse(response=null) { 4 | const error = new Error('Invalid Response'); 5 | error.response = response; 6 | return error; 7 | } 8 | 9 | InvalidResponse.prototype = Object.create(Error.prototype); 10 | 11 | export async function handleNodeExceptions(e) { 12 | const insufficientFundsError = /TransactionPool\.Remember: transaction [A-Z0-9]+: underflow on subtracting \d+ from sender amount \d+/g; 13 | const belowMinimumError = /TransactionPool\.Remember: transaction [A-Z0-9]+: account [A-Z0-9]+ balance \d+ below min (\d+)/g; 14 | const maxOptedInApps = /TransactionPool\.Remember: transaction [A-Z0-9]+: cannot opt in app [A-Z0-9]+ for [A-Z0-9]+: max opted-in apps per acct is \d+/g; 15 | let match; 16 | if (e.message.match(insufficientFundsError)) { 17 | emitError('Insufficient funds'); 18 | throw e; 19 | // eslint-disable-next-line no-constant-condition 20 | } else if ((match = [...e.message.matchAll(belowMinimumError)]).length > 0) { 21 | const minimumBalance = (Number(match[0][1]) / (10 ** 6)).toFixed(6); 22 | emitError(`After this transaction, the balance would fall below the minimum of ${minimumBalance} Algos`); 23 | throw e; 24 | } else if (e.message.match(maxOptedInApps)) { 25 | emitError('Maximum amount of opted-in applications per account exceeded. Use a different account'); 26 | throw e; 27 | } 28 | emitError('Unexpected error occured while sending transaction'); 29 | throw e; 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/services/internal.js: -------------------------------------------------------------------------------- 1 | import { emitError } from '@/utils/errors'; 2 | import { InvalidResponse } from '@/services/base'; 3 | import store from '@/store/index'; 4 | import { NeedsToRetry, retryWhenNeeded } from '@/utils/retry'; 5 | import { BACKEND_URL } from '@/config'; 6 | 7 | class InternalService { 8 | constructor() { 9 | this.url = BACKEND_URL; 10 | } 11 | 12 | async getCompiledProxy(proxyId) { 13 | return await this.retryingPost('/api/contracts/compile_proxy/', JSON.stringify({ 14 | proxy_id: proxyId 15 | }), { 16 | 'Content-Type': 'application/json' 17 | }); 18 | } 19 | 20 | async getCompiledClear() { 21 | return await this.retryingPost('/api/contracts/compile_clear/', null, { 22 | 'Content-Type': 'application/json' 23 | }); 24 | } 25 | 26 | async getCompiledManager() { 27 | return await this.retryingPost('/api/contracts/compile_manager/', null, { 28 | 'Content-Type': 'application/json' 29 | }); 30 | } 31 | 32 | async getCompiledEscrow(appId, usdcId, nftId) { 33 | return await this.retryingPost('/api/contracts/compile_escrow/', JSON.stringify({ 34 | app_id: appId, 35 | usdc_id: usdcId, 36 | nft_id: nftId 37 | }), { 38 | 'Content-Type': 'application/json' 39 | }); 40 | } 41 | 42 | async getAsset(guid) { 43 | return await this.get(`/api/assets/${guid}/`); 44 | } 45 | 46 | async getUser(address) { 47 | return await this.get(`/api/users/${address}/`, {}, {}, false); 48 | } 49 | 50 | async getAssets(params={}) { 51 | return await this.get('/api/assets/', {}, params); 52 | } 53 | 54 | async getOperations(params={}) { 55 | return await this.get('/api/operations/', {}, params); 56 | } 57 | 58 | async getOperation(operationId) { 59 | return await this.get(`/api/operations/${operationId}`); 60 | } 61 | 62 | async getCsrfToken(params={}) { 63 | return await this.get('/api/token/', {}, params); 64 | } 65 | 66 | async validateAsset(data) { 67 | const formData = new FormData(); 68 | for (const key in data) { 69 | formData.append(key, data[key]); 70 | } 71 | return await this.post('/api/assets/validate_asset/', formData, {}, {}, false); 72 | } 73 | 74 | async sendOperationTx(data) { 75 | return await this.retryingPost('/api/operations/send_tx/', JSON.stringify(data), { 76 | 'Content-Type': 'application/json' 77 | }); 78 | } 79 | 80 | async submitContract(data) { 81 | return await this.retryingPost('/api/contracts/submit_contract/', JSON.stringify(data), { 82 | 'Content-Type': 'application/json' 83 | }); 84 | } 85 | 86 | async submitConfiguration(data) { 87 | return await this.retryingPost('/api/contracts/submit_configuration/', JSON.stringify(data), { 88 | 'Content-Type': 'application/json' 89 | }); 90 | } 91 | 92 | async submitAsset(data) { 93 | const formData = new FormData(); 94 | for (const key in data) { 95 | formData.append(key, data[key]); 96 | } 97 | return await this.retryingPost('/api/assets/submit_asset/', formData, {}); 98 | } 99 | 100 | getDefaultHeaders() { 101 | if (store.getters['algorand/account']) { 102 | return { 103 | 'X-View-As': store.getters['algorand/account'] 104 | }; 105 | } else { 106 | return {}; 107 | } 108 | } 109 | 110 | async get(path, headers={}, params={}, throwExceptions=true) { 111 | try { 112 | return await this.baseRequest('GET', path, undefined, headers, params, throwExceptions); 113 | } catch (e) { 114 | emitError('Could not get information from internal service'); 115 | throw e; 116 | } 117 | } 118 | 119 | async addCsrfToken(headers) { 120 | let csrfToken = store.getters['internal/csrfToken']; 121 | if (!csrfToken) { 122 | await this.store.dispatch('internal/FETCH_CSRF_TOKEN'); 123 | csrfToken = store.getters['internal/csrfToken']; 124 | } 125 | return Object.assign({}, headers, { 126 | 'X-CSRFToken': csrfToken 127 | }); 128 | } 129 | 130 | async retryingPost(path, payload, headers={}, params={}, throwExceptions=true) { 131 | try { 132 | return await retryWhenNeeded(async () => { 133 | return await this.baseRequest('POST', path, payload, await this.addCsrfToken(headers), params, throwExceptions); 134 | }); 135 | } catch (e) { 136 | emitError('Could not broadcast information to internal service'); 137 | throw e; 138 | } 139 | } 140 | 141 | async post(path, payload, headers={}, params={}, throwExceptions=true) { 142 | try { 143 | return await this.baseRequest('POST', path, payload, await this.addCsrfToken(headers), params, throwExceptions); 144 | } catch (e) { 145 | emitError('Could not broadcast information to internal service'); 146 | throw e; 147 | } 148 | } 149 | 150 | async baseRequest(method, path, payload, headers={}, params={}, throwExceptions=true) { 151 | let fullPath = `${this.url}${path}`; 152 | if (Object.keys(params).length > 0) { 153 | fullPath = `${fullPath}?${new URLSearchParams(params).toString()}`; 154 | } 155 | const response = await fetch(fullPath, { 156 | method: method, 157 | body: payload, 158 | mode: 'cors', 159 | headers: Object.assign({}, this.getDefaultHeaders(), headers) 160 | }); 161 | return await this.processResponse(response, throwExceptions); 162 | } 163 | 164 | async processResponse(response, throwExceptions) { 165 | let responseJson; 166 | try { 167 | responseJson = await response.json(); 168 | } catch(e) { 169 | if (response.status === 500) { 170 | throw new NeedsToRetry(); 171 | } else if (response.status !== 200 && response.status !== 201 && throwExceptions) { 172 | throw new InvalidResponse(response); 173 | } 174 | return; 175 | } 176 | if (responseJson && responseJson.retry) { 177 | throw new NeedsToRetry(); 178 | } else if (response.status !== 200 && response.status !== 201 && throwExceptions) { 179 | throw new InvalidResponse(response); 180 | } 181 | return responseJson; 182 | } 183 | } 184 | 185 | export const internalService = new InternalService(); 186 | -------------------------------------------------------------------------------- /frontend/src/services/myAlgoWallet.js: -------------------------------------------------------------------------------- 1 | import { emitError } from '@/utils/errors'; 2 | import eventBus from '@/utils/eventBus'; 3 | import MyAlgo from '@randlabs/myalgo-connect'; 4 | import { algoExplorer } from '@/services/algoExplorer'; 5 | import { base64ToUint8Array, Uint8ArrayToBase64 } from '@/utils/encoding'; 6 | import { TX_FORMAT, unwrapTxs } from '@/utils/transactions'; 7 | import { handleNodeExceptions } from '@/services/base'; 8 | 9 | function NoAccounts() { 10 | return new Error('No accounts'); 11 | } 12 | 13 | NoAccounts.prototype = Object.create(Error.prototype); 14 | 15 | export class MyAlgoWallet { 16 | constructor() { 17 | this.myAlgoWallet = new MyAlgo(); 18 | this.accountList = []; 19 | } 20 | 21 | async connect() { 22 | try { 23 | this.accountList = await this.myAlgoWallet.connect(); 24 | } catch (e) { 25 | emitError('Could not connect to myAlgo'); 26 | throw e; 27 | } 28 | } 29 | 30 | async sign(txs, description=null, separate=false) { 31 | try { 32 | const txsCount = txs.length; 33 | if (description) { 34 | if (txsCount > 1) { 35 | eventBus.$emit('set-action-message', `Signing ${description}...`); 36 | } else { 37 | eventBus.$emit('set-action-message', `Signing ${description}s...`); 38 | } 39 | } 40 | let signedTxs = []; 41 | 42 | if (separate && txsCount > 1) { 43 | let count = 1; 44 | for (const tx of txs) { 45 | eventBus.$emit('set-action-message', `Signing ${count} of ${txs.length} ${description}s...`); 46 | signedTxs.push((await this.sign([tx]))[0]); 47 | count += 1; 48 | } 49 | return signedTxs; 50 | } 51 | txs = unwrapTxs(txs, TX_FORMAT.MyAlgo); 52 | 53 | signedTxs = await this.myAlgoWallet.signTransaction(txs.length === 1 ? txs[0] : txs); 54 | if (signedTxs.constructor === Object) 55 | { 56 | return [{ 57 | blob: Uint8ArrayToBase64(signedTxs.blob) 58 | }]; 59 | } else { 60 | return signedTxs.map((tx) => { 61 | tx.blob = Uint8ArrayToBase64(tx.blob); 62 | return tx; 63 | }); 64 | } 65 | } catch (e) { 66 | if (e.message === 'Can not open popup window - blocked') { 67 | emitError('Your browser is blocking the pop-up windows on this site. Please adjust your browser settings'); 68 | } else { 69 | emitError('Transactions could not be signed'); 70 | } 71 | throw e; 72 | } 73 | } 74 | 75 | // eslint-disable-next-line no-unused-vars 76 | async accounts(params) { 77 | if (this.accountList.length > 0) { 78 | return this.accountList; 79 | } else { 80 | emitError('Could not get information about accounts'); 81 | throw new NoAccounts(); 82 | } 83 | } 84 | 85 | async algod(params) { 86 | try { 87 | return algoExplorer.get(params.path); 88 | } catch (e) { 89 | emitError('Could not get information from the Algorand blockchain'); 90 | throw e; 91 | } 92 | } 93 | 94 | async send(params, showSucess = true) { 95 | try { 96 | eventBus.$emit('set-action-message', 'Sending...'); 97 | const tx = await algoExplorer.post('/v2/transactions', base64ToUint8Array(params.tx), { 98 | 'Content-Type': 'application/x-binary' 99 | }); 100 | if (showSucess) { 101 | eventBus.$emit('transaction-success', tx.txId); 102 | } 103 | return tx; 104 | } catch (e) { 105 | try { 106 | e.message = (await e.response.json())['message']; 107 | } finally { 108 | await handleNodeExceptions(e); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /frontend/src/store/algorand/getters.js: -------------------------------------------------------------------------------- 1 | import { 2 | getMappedGlobalState, 3 | getMappedUserAssets, 4 | getMappedUserCreatedAssets, 5 | getMappedUserStates 6 | } from '@/utils/format'; 7 | 8 | export function rawStore(state) { 9 | return { 10 | walletManager: state.walletManager, 11 | connected: state.connected, 12 | accounts: state.accounts, 13 | account: state.account, 14 | accountData: state.accountData, 15 | accountDataCache: state.accountData, 16 | fetchedAccounts: state.fetchedAccounts, 17 | currentApplicationId: state.currentApplicationId, 18 | applicationDataCache: state.applicationDataCache, 19 | walletServices: state.walletServices, 20 | walletName: state.walletName, 21 | // Action queue 22 | pendingAction: state.pendingAction, 23 | pendingActionMessage: state.pendingActionMessage, 24 | pendingUpdate: state.pendingUpdate, 25 | actionResult: state.actionResult 26 | }; 27 | } 28 | 29 | export function account(state) { 30 | return state.account; 31 | } 32 | 33 | export function userStates(state) { 34 | if (!state.accountData) { 35 | return {}; 36 | } 37 | return getMappedUserStates(state.accountData); 38 | } 39 | 40 | export function userAssets(state) { 41 | if (!state.accountData) { 42 | return {}; 43 | } 44 | return getMappedUserAssets(state.accountData); 45 | } 46 | 47 | export function userCreatedAssets(state) { 48 | if (!state.accountData) { 49 | return {}; 50 | } 51 | return getMappedUserCreatedAssets(state.accountData); 52 | } 53 | 54 | export function accounts(state) { 55 | if (!state.accounts) { 56 | return []; 57 | } 58 | return state.accounts.map((value) => { 59 | return value.address; 60 | }); 61 | } 62 | 63 | export function isReady(state) { 64 | return state.account && state.accountData; 65 | } 66 | 67 | export function isReadyToTransact(state) { 68 | return state.account && state.accountData && !state.pendingUpdate; 69 | } 70 | 71 | export function algoBalance(state) { 72 | if (!state.accountData) { 73 | return 0; 74 | } 75 | const amount = state.accountData['amount-without-pending-rewards']; 76 | if (!amount) { 77 | return 0; 78 | } 79 | return amount; 80 | } 81 | 82 | export function assetBalances(state, getters) { 83 | if (!state.accountData) { 84 | return {}; 85 | } 86 | let balances = {}; 87 | Object.keys(getters.userAssets).forEach((assetIndex) => { 88 | let userAsset = getters.userAssets[assetIndex]; 89 | balances[assetIndex] = userAsset.amount; 90 | }); 91 | return balances; 92 | } 93 | 94 | export function applicationData(state) { 95 | if (!state.currentApplicationId) { 96 | return {}; 97 | } 98 | if(!state.applicationDataCache[state.currentApplicationId]) { 99 | return {}; 100 | } 101 | return getMappedGlobalState(state.applicationDataCache[state.currentApplicationId]); 102 | } 103 | -------------------------------------------------------------------------------- /frontend/src/store/algorand/index.js: -------------------------------------------------------------------------------- 1 | import state from './state'; 2 | import * as getters from './getters'; 3 | import * as mutations from './mutations'; 4 | import * as actions from './actions'; 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/src/store/algorand/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export function SET_SERVICE_INSTANCE(state, instance) { 4 | state.walletManager = instance; 5 | } 6 | 7 | export function SET_CONNECTED(state, isConnected) { 8 | state.connected = isConnected; 9 | } 10 | 11 | export function SET_ACCOUNTS(state, accounts) { 12 | state.accounts = accounts; 13 | state.fetchedAccounts = true; 14 | } 15 | 16 | export function SET_ACCOUNT(state, account) { 17 | state.account = account; 18 | if (account) { 19 | localStorage.setItem('account', account); 20 | } else { 21 | localStorage.removeItem('account'); 22 | } 23 | } 24 | 25 | export function SET_CURRENT_ACCOUNT_DATA(state, accountData) { 26 | state.accountData = accountData; 27 | } 28 | 29 | export function SET_CURRENT_APPLICATION_ID(state, applicationId) { 30 | state.currentApplicationId = applicationId; 31 | } 32 | 33 | export function CACHE_ACCOUNT_DATA(state, { accountAddress, accountData }) { 34 | Vue.set(state.accountDataCache, accountAddress, accountData); 35 | } 36 | 37 | export function CACHE_APPLICATION_DATA(state, { applicationIndex, applicationData }) { 38 | Vue.set(state.applicationDataCache, applicationIndex, applicationData); 39 | } 40 | 41 | export function SET_PENDING_UPDATE(state, pendingUpdate) { 42 | state.pendingUpdate = pendingUpdate; 43 | } 44 | 45 | export function SET_PENDING_ACTION(state, pendingAction) { 46 | state.pendingAction = pendingAction; 47 | } 48 | 49 | export function SET_ACTION_RESULT(state, actionResult) { 50 | state.actionResult = actionResult; 51 | } 52 | 53 | export function SET_PENDING_VERIFICATION_FUNC(state, verificationFunc) { 54 | state.pendingVerificationFunc = verificationFunc; 55 | } 56 | 57 | export function SET_PENDING_ACTION_MESSAGE(state, pendingActionMessage) { 58 | state.pendingActionMessage = pendingActionMessage; 59 | } 60 | 61 | export function SET_ACTION_QUEUE(state, queue) { 62 | state.actionQueue = queue; 63 | } 64 | 65 | export function ADD_WALLET_SERVICE(state, { walletName, walletService }) { 66 | Vue.set(state.walletServices, walletName, walletService); 67 | } 68 | 69 | export function SET_WALLET_NAME(state, walletName) { 70 | state.walletName = walletName; 71 | if (walletName) { 72 | localStorage.setItem('wallet', walletName); 73 | } else { 74 | localStorage.removeItem('wallet'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/store/algorand/state.js: -------------------------------------------------------------------------------- 1 | import WalletManager from '@/services/walletManager'; 2 | import { ALGORAND_LEDGER } from '@/config'; 3 | import { MyAlgoWallet } from '@/services/myAlgoWallet'; 4 | import MyAlgo from '@randlabs/myalgo-connect'; 5 | 6 | export default function() { 7 | return { 8 | walletManager: new WalletManager(ALGORAND_LEDGER), 9 | walletServices: { 10 | myAlgo: new MyAlgoWallet(new MyAlgo()) 11 | }, 12 | connected: false, 13 | walletName: localStorage.getItem('wallet'), 14 | accounts: [], 15 | fetchedAccounts: false, 16 | account: localStorage.getItem('account'), 17 | accountDataCache: {}, 18 | accountData: null, 19 | currentApplicationId: null, 20 | applicationDataCache: {}, 21 | // Action queue 22 | actionQueue: [], 23 | actionResult: {}, 24 | pendingUpdate: false, 25 | pendingAction: null, 26 | pendingActionMessage: null, 27 | pendingVerificationFunc: null 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import algorand from './algorand'; 5 | import internal from './internal'; 6 | 7 | Vue.use(Vuex); 8 | 9 | const store = new Vuex.Store({ 10 | modules: { 11 | algorand, 12 | internal 13 | }, 14 | 15 | // enable strict mode (adds overhead!) 16 | // for dev mode only 17 | strict: process.env.DEV 18 | }); 19 | 20 | export default store; 21 | -------------------------------------------------------------------------------- /frontend/src/store/internal/actions.js: -------------------------------------------------------------------------------- 1 | import { internalService } from '@/services/internal'; 2 | 3 | export async function FETCH_USER_DATA({ commit }, { accountAddress }) { 4 | try { 5 | const userData = await internalService.getUser(accountAddress); 6 | await commit('SET_USER_DATA', userData); 7 | await commit('SET_FETCHED_USER_DATA', true); 8 | } catch (e) { 9 | await commit('SET_USER_DATA', {}); 10 | await commit('SET_FETCHED_USER_DATA', true); 11 | throw e; 12 | } 13 | } 14 | 15 | export async function FETCH_CSRF_TOKEN({ commit }) { 16 | try { 17 | const response = await internalService.getCsrfToken(); 18 | await commit('SET_CSRF_TOKEN', response['token']); 19 | } catch (e) { 20 | await commit('SET_CSRF_TOKEN', ''); 21 | throw e; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/store/internal/getters.js: -------------------------------------------------------------------------------- 1 | export function isStaff(state) { 2 | return state.userData['is_staff'] || false; 3 | } 4 | 5 | export function isFetched(state) { 6 | return state.fetchedUserData; 7 | } 8 | 9 | export function csrfToken(state) { 10 | return state.csrfToken; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/store/internal/index.js: -------------------------------------------------------------------------------- 1 | import state from './state'; 2 | import * as getters from './getters'; 3 | import * as mutations from './mutations'; 4 | import * as actions from './actions'; 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/src/store/internal/mutations.js: -------------------------------------------------------------------------------- 1 | export function SET_USER_DATA(state, userData) { 2 | state.userData = userData; 3 | } 4 | 5 | export function SET_CSRF_TOKEN(state, csrfToken) { 6 | state.csrfToken = csrfToken; 7 | } 8 | 9 | export function SET_FETCHED_USER_DATA(state, fetchedUserData) { 10 | state.fetchedUserData = fetchedUserData; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/store/internal/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | userData: {}, 4 | fetchedUserData: false, 5 | csrfToken: '' 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const CONFIGURE = 'C'; 2 | export const SET_PRICE = 'S'; 3 | export const BID = 'B'; 4 | export const BUY_NOW = 'BN'; 5 | export const SELL_NOW = 'SN'; 6 | 7 | export const BID_PRICE = 'B'; 8 | -------------------------------------------------------------------------------- /frontend/src/utils/encoding.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | 3 | export function base64ToUint8Array(base64) { 4 | return new Uint8Array(atob(base64).split('').map(x => x.charCodeAt(0))); 5 | } 6 | 7 | export function Uint8ArrayToBase64(array) { 8 | return btoa(String.fromCharCode.apply(null, array)); 9 | } 10 | 11 | function bufferToUint8(buf) { 12 | const ab = new ArrayBuffer(buf.length); 13 | const view = new Uint8Array(ab); 14 | for (let i = 0; i < buf.length; ++i) { 15 | view[i] = buf[i]; 16 | } 17 | return view; 18 | } 19 | 20 | export function encodeArrayForSDK(decodedArray) { 21 | if (!decodedArray) { 22 | return decodedArray; 23 | } 24 | const encoder = new TextEncoder('ascii'); 25 | return decodedArray.map((value) => { 26 | if (typeof value === 'number') { 27 | return uint64ToBigEndian(value); 28 | } 29 | return encoder.encode(value); 30 | }); 31 | } 32 | 33 | export function uint64ToBigEndian(x) { 34 | const buff = Buffer.alloc(8); 35 | buff.writeUIntBE(x, 0, 8); 36 | return bufferToUint8(buff); 37 | } 38 | 39 | export function encodeArrayForSigner(decodedArray) { 40 | if (!decodedArray) { 41 | return decodedArray; 42 | } 43 | return decodedArray.map((value) => { 44 | if (typeof value === 'number') { 45 | return btoa(String.fromCharCode.apply(null, uint64ToBigEndian(value))); 46 | } 47 | return btoa(value); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/utils/errors.js: -------------------------------------------------------------------------------- 1 | import eventBus from '@/utils/eventBus'; 2 | 3 | export function emitError(message) { 4 | eventBus.$emit('open-alert', { 5 | type: 'error', 6 | message: message 7 | }); 8 | eventBus.$emit('close-asset-modals'); 9 | eventBus.$emit('close-modals'); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/utils/eventBus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | const eventBus = new Vue(); 3 | export default eventBus; 4 | -------------------------------------------------------------------------------- /frontend/src/utils/format.js: -------------------------------------------------------------------------------- 1 | export function getMappedUserAssets(accountData) { 2 | const assets = accountData['assets']; 3 | return Object.assign({}, ...assets.map((value) => { 4 | return { 5 | [value['asset-id']]: value 6 | }; 7 | })); 8 | } 9 | 10 | export function getMappedUserCreatedAssets(accountData) { 11 | const assets = accountData['created-assets']; 12 | return Object.assign({}, ...assets.map((value) => { 13 | return { 14 | [value['index']]: value 15 | }; 16 | })); 17 | } 18 | 19 | export function getMappedGlobalState(applicationData) { 20 | if (!applicationData || !applicationData['params'] || !applicationData['params']['global-state']) { 21 | return {}; 22 | } 23 | const state = applicationData['params']['global-state']; 24 | return decodeState(state); 25 | } 26 | 27 | export function decodeState(state) { 28 | return Object.assign({}, ...state.map(rawValue => { 29 | const key = atob(rawValue.key); 30 | let value; 31 | if (rawValue.value.type === 1) { 32 | value = rawValue.value.bytes; 33 | } else if (rawValue.value.type === 2) { 34 | value = Number(rawValue.value.uint); 35 | } 36 | return { 37 | [key]: value 38 | }; 39 | })); 40 | } 41 | 42 | export function getMappedUserStates(accountData) { 43 | const appStates = accountData['apps-local-state']; 44 | return Object.assign({}, ...appStates.map(value => { 45 | return { 46 | [value.id]: getMappedUserState(value) 47 | }; 48 | })); 49 | } 50 | 51 | function getMappedUserState(state) { 52 | if (!state) { 53 | return {}; 54 | } 55 | const keyValues = state['key-value']; 56 | return decodeState(keyValues); 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/utils/operations.js: -------------------------------------------------------------------------------- 1 | import { internalService } from '@/services/internal'; 2 | import { emitError } from '@/utils/errors'; 3 | 4 | 5 | export async function checkIfOperationIsCompleted({ state }) { 6 | const actionResult = state.actionResult; 7 | const operationId = actionResult['operationId']; 8 | const response = await internalService.getOperation(operationId); 9 | if (response['is_pending'] === false) { 10 | if (response['is_executed'] === true) { 11 | return true; 12 | } else { 13 | emitError('Operation failed'); 14 | return true; 15 | } 16 | } 17 | return false; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/utils/precision.js: -------------------------------------------------------------------------------- 1 | import { USDC_DECIMAL_POINTS } from '@/config'; 2 | 3 | export function toDisplayValue(rawValue) { 4 | return Number(rawValue) / (10 ** USDC_DECIMAL_POINTS); 5 | } 6 | 7 | export function toRawValue(displayValue) { 8 | return Math.floor(Number(displayValue) * (10 ** USDC_DECIMAL_POINTS)); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/utils/retry.js: -------------------------------------------------------------------------------- 1 | export async function retryWhenNeeded(method, maxRetries = 3, delay = 5000) { 2 | try { 3 | return await method(); 4 | } catch (e) { 5 | if (maxRetries <= 1 || !e.retry) { 6 | throw e; 7 | } 8 | return new Promise((resolve, reject) => { 9 | window.setTimeout( 10 | async () => { 11 | try { 12 | resolve(await retryWhenNeeded(method, maxRetries - 1, delay)); 13 | } catch (e) { 14 | reject(e); 15 | } 16 | }, delay 17 | ); 18 | }); 19 | } 20 | } 21 | 22 | export function NeedsToRetry() { 23 | const error = new Error('Needs to retry'); 24 | error.retry = true; 25 | return error; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/utils/validation.js: -------------------------------------------------------------------------------- 1 | import store from '@/store'; 2 | import eventBus from '@/utils/eventBus'; 3 | 4 | function InsufficientFunds() { 5 | return new Error('Insufficient Funds'); 6 | } 7 | 8 | InsufficientFunds.prototype = Object.create(Error.prototype); 9 | 10 | export async function validateTx(txs) { 11 | await store.dispatch('algorand/FETCH_ACCOUNT_DATA', {}); 12 | const algoSpending = countAlgoSpending(txs); 13 | await validateAlgoBalance(algoSpending); 14 | 15 | const assetSpendings = countAssetSpending(txs); 16 | await validateAssetBalances(assetSpendings); 17 | } 18 | 19 | export function validateAlgoBalance(algoSpending, throwException=true) { 20 | if (algoSpending > getAlgoBalance()) { 21 | if (!throwException) { 22 | return false; 23 | } 24 | emitInsufficientFundsError(); 25 | throw InsufficientFunds(); 26 | } 27 | return true; 28 | } 29 | 30 | export function validateAssetBalances(assetSpendings, throwException=true) { 31 | const assetBalances = getAssetBalances(); 32 | for (let assetIndex of Object.keys(assetSpendings)) { 33 | let assetSpending = assetSpendings[assetIndex]; 34 | if (!assetBalances[assetIndex] || assetSpending > assetBalances[assetIndex]) { 35 | if (!throwException) { 36 | return false; 37 | } 38 | emitInsufficientFundsError(); 39 | throw InsufficientFunds(); 40 | } 41 | } 42 | return true; 43 | } 44 | 45 | function emitInsufficientFundsError() { 46 | eventBus.$emit('open-alert', { 47 | type: 'error', 48 | message: 'Insufficient funds' 49 | }); 50 | } 51 | 52 | function getAlgoBalance() { 53 | return store.getters['algorand/algoBalance']; 54 | } 55 | 56 | function getAssetBalances() { 57 | return store.getters['algorand/assetBalances']; 58 | } 59 | 60 | function getAccountAddress() { 61 | return store.getters['algorand/rawStore'].account; 62 | } 63 | 64 | function countAlgoSpending(txs) { 65 | let totalSpending = 0; 66 | txs.forEach((tx) => { 67 | if (tx.from !== getAccountAddress()) { 68 | return; 69 | } 70 | totalSpending += tx.fee; 71 | if (tx.amount && tx.type !== 'axfer') { 72 | totalSpending += tx.amount; 73 | } 74 | }); 75 | return totalSpending; 76 | } 77 | 78 | function countAssetSpending(txs) { 79 | let spending = {}; 80 | txs.forEach((tx) => { 81 | if (tx.from !== getAccountAddress()) { 82 | return; 83 | } 84 | if (tx.amount && tx.type === 'axfer') { 85 | if (!spending[tx.assetIndex]) { 86 | spending[tx.assetIndex] = 0; 87 | } 88 | spending[tx.assetIndex] += tx.amount; 89 | } 90 | }); 91 | return spending; 92 | } 93 | -------------------------------------------------------------------------------- /frontend/src/views/AddAsset.vue: -------------------------------------------------------------------------------- 1 | 6 | 16 | -------------------------------------------------------------------------------- /frontend/src/views/AllItems.vue: -------------------------------------------------------------------------------- 1 | 14 | 49 | -------------------------------------------------------------------------------- /frontend/src/views/AssetDetails.vue: -------------------------------------------------------------------------------- 1 | 6 | 16 | -------------------------------------------------------------------------------- /frontend/src/views/AssetList.vue: -------------------------------------------------------------------------------- 1 | 64 | 138 | 143 | -------------------------------------------------------------------------------- /frontend/src/views/DeployContract.vue: -------------------------------------------------------------------------------- 1 | 6 | 16 | -------------------------------------------------------------------------------- /frontend/src/views/ForSale.vue: -------------------------------------------------------------------------------- 1 | 14 | 50 | -------------------------------------------------------------------------------- /frontend/src/views/HighestBids.vue: -------------------------------------------------------------------------------- 1 | 14 | 50 | -------------------------------------------------------------------------------- /frontend/src/views/RecentlyAdded.vue: -------------------------------------------------------------------------------- 1 | 14 | 49 | -------------------------------------------------------------------------------- /frontend/src/views/UserAssetList.vue: -------------------------------------------------------------------------------- 1 | 20 | 74 | -------------------------------------------------------------------------------- /frontend/src/views/UserBidList.vue: -------------------------------------------------------------------------------- 1 | 23 | 86 | -------------------------------------------------------------------------------- /frontend/src/views/UserCreatedAssetList.vue: -------------------------------------------------------------------------------- 1 | 37 | 101 | -------------------------------------------------------------------------------- /frontend/tailwind.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors'); 2 | 3 | module.exports = { 4 | purge: [], 5 | darkMode: false, // or 'media' or 'class' 6 | theme: { 7 | extend: { 8 | colors: { 9 | rose: colors.rose 10 | } 11 | }, 12 | screens: { 13 | 'xs': '320px', 14 | 'sm': '640px', 15 | 'md': '768px', 16 | 'lg': '1024px', 17 | 'xl': '1280px', 18 | '2xl': '1536px' 19 | } 20 | }, 21 | variants: { 22 | extend: { 23 | opacity: ['disabled'], 24 | cursor: ['disabled'], 25 | ringWidth: ['hover', 'focus'] 26 | } 27 | }, 28 | plugins: [require('@tailwindcss/forms'), require('@tailwindcss/aspect-ratio')] 29 | }; 30 | --------------------------------------------------------------------------------