├── .browserslistrc ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── extra-webpack.config.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── claim │ │ ├── claim.component.css │ │ ├── claim.component.html │ │ ├── claim.component.spec.ts │ │ └── claim.component.ts │ ├── constants.service.spec.ts │ ├── constants.service.ts │ ├── contract.service.spec.ts │ ├── contract.service.ts │ ├── create │ │ ├── create.component.css │ │ ├── create.component.html │ │ ├── create.component.spec.ts │ │ └── create.component.ts │ ├── header │ │ ├── header.component.css │ │ ├── header.component.html │ │ ├── header.component.spec.ts │ │ └── header.component.ts │ ├── helpers.service.spec.ts │ ├── helpers.service.ts │ ├── home │ │ ├── home.component.css │ │ ├── home.component.html │ │ ├── home.component.spec.ts │ │ └── home.component.ts │ ├── profile │ │ ├── profile.component.css │ │ ├── profile.component.html │ │ ├── profile.component.spec.ts │ │ └── profile.component.ts │ ├── wallet.service.spec.ts │ ├── wallet.service.ts │ ├── web3.ts │ └── web3Enabled.ts ├── assets │ ├── .gitkeep │ ├── abis │ │ ├── Astrodrop.json │ │ ├── AstrodropFactory.json │ │ ├── ERC20.json │ │ └── ERC721.json │ ├── css │ │ └── sakura-vader.css │ ├── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ └── site.webmanifest │ └── json │ │ └── contracts.json ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── libs │ ├── ipfs-mini.js │ ├── ipfs-search-tree │ │ ├── interfaces.ts │ │ ├── ipfs-helper.ts │ │ ├── local-ipfs-search-tree.ts │ │ └── remote-ipfs-search-tree.ts │ └── merkle-tree │ │ ├── balance-tree.ts │ │ ├── merkle-tree.ts │ │ └── parse-balance-map.ts ├── main.ts ├── polyfills.ts ├── styles.css └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MerkleDrop 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "merkle-drop": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-builders/custom-webpack:browser", 15 | "options": { 16 | "outputPath": "dist/merkle-drop", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "aot": true, 22 | "assets": [ 23 | "src/favicon.ico", 24 | "src/assets" 25 | ], 26 | "styles": [ 27 | "src/assets/css/sakura-vader.css", 28 | "src/styles.css", 29 | "node_modules/prismjs/themes/prism-okaidia.css", 30 | "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css", 31 | "node_modules/katex/dist/katex.min.css" 32 | ], 33 | "scripts": [ 34 | "node_modules/marked/lib/marked.js", 35 | "node_modules/prismjs/prism.js", 36 | "node_modules/prismjs/components/prism-solidity.min.js", 37 | "node_modules/prismjs/components/prism-json.min.js", 38 | "node_modules/prismjs/components/prism-haskell.min.js", 39 | "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js", 40 | "node_modules/emoji-toolkit/lib/js/joypixels.min.js", 41 | "node_modules/katex/dist/katex.min.js" 42 | ], 43 | "customWebpackConfig": { 44 | "path": "./extra-webpack.config.js" 45 | } 46 | }, 47 | "configurations": { 48 | "production": { 49 | "fileReplacements": [ 50 | { 51 | "replace": "src/environments/environment.ts", 52 | "with": "src/environments/environment.prod.ts" 53 | } 54 | ], 55 | "optimization": true, 56 | "outputHashing": "all", 57 | "sourceMap": false, 58 | "extractCss": true, 59 | "namedChunks": false, 60 | "extractLicenses": true, 61 | "vendorChunk": false, 62 | "buildOptimizer": true, 63 | "budgets": [ 64 | { 65 | "type": "initial", 66 | "maximumWarning": "2mb", 67 | "maximumError": "5mb" 68 | }, 69 | { 70 | "type": "anyComponentStyle", 71 | "maximumWarning": "6kb", 72 | "maximumError": "10kb" 73 | } 74 | ] 75 | } 76 | } 77 | }, 78 | "serve": { 79 | "builder": "@angular-builders/custom-webpack:dev-server", 80 | "options": { 81 | "browserTarget": "merkle-drop:build" 82 | }, 83 | "configurations": { 84 | "production": { 85 | "browserTarget": "merkle-drop:build:production" 86 | } 87 | } 88 | }, 89 | "extract-i18n": { 90 | "builder": "@angular-devkit/build-angular:extract-i18n", 91 | "options": { 92 | "browserTarget": "merkle-drop:build" 93 | } 94 | }, 95 | "test": { 96 | "builder": "@angular-devkit/build-angular:karma", 97 | "options": { 98 | "main": "src/test.ts", 99 | "polyfills": "src/polyfills.ts", 100 | "tsConfig": "tsconfig.spec.json", 101 | "karmaConfig": "karma.conf.js", 102 | "assets": [ 103 | "src/favicon.ico", 104 | "src/assets" 105 | ], 106 | "styles": [ 107 | "src/styles.css" 108 | ], 109 | "scripts": [] 110 | } 111 | }, 112 | "lint": { 113 | "builder": "@angular-devkit/build-angular:tslint", 114 | "options": { 115 | "tsConfig": [ 116 | "tsconfig.app.json", 117 | "tsconfig.spec.json", 118 | "e2e/tsconfig.json" 119 | ], 120 | "exclude": [ 121 | "**/node_modules/**" 122 | ] 123 | } 124 | }, 125 | "e2e": { 126 | "builder": "@angular-devkit/build-angular:protractor", 127 | "options": { 128 | "protractorConfig": "e2e/protractor.conf.js", 129 | "devServerTarget": "merkle-drop:serve" 130 | }, 131 | "configurations": { 132 | "production": { 133 | "devServerTarget": "merkle-drop:serve:production" 134 | } 135 | } 136 | }, 137 | "deploy": { 138 | "builder": "angular-cli-ghpages:deploy", 139 | "options": { 140 | "repo": "git@github.com:ZeframLou/astrodrop-production.git", 141 | "branch": "main", 142 | "cname": "astrodrop.org" 143 | } 144 | } 145 | } 146 | } 147 | }, 148 | "defaultProject": "merkle-drop" 149 | } -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ 31 | spec: { 32 | displayStacktrace: StacktraceOption.PRETTY 33 | } 34 | })); 35 | } 36 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('merkle-drop app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /extra-webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: { 3 | stream: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/merkle-drop'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "merkle-drop", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~10.2.0", 15 | "@angular/common": "~10.2.0", 16 | "@angular/compiler": "~10.2.0", 17 | "@angular/core": "~10.2.0", 18 | "@angular/forms": "~10.2.0", 19 | "@angular/platform-browser": "~10.2.0", 20 | "@angular/platform-browser-dynamic": "~10.2.0", 21 | "@angular/router": "~10.2.0", 22 | "@pinata/sdk": "^1.1.23", 23 | "base-58": "^0.0.1", 24 | "bignumber.js": "^9.0.1", 25 | "bnc-notify": "^1.9.1", 26 | "bnc-onboard": "^1.34.1", 27 | "ethereumjs-util": "^7.0.7", 28 | "ethers": "^5.4.6", 29 | "graphql-request": "^3.5.0", 30 | "ipfs-only-hash": "^4.0.0", 31 | "ngx-markdown": "^10.1.1", 32 | "node-fetch": "^2.6.1", 33 | "papaparse": "^5.3.0", 34 | "rxjs": "~6.6.0", 35 | "tslib": "^2.0.0", 36 | "web3": "^1.5.2", 37 | "zone.js": "~0.10.2" 38 | }, 39 | "devDependencies": { 40 | "@angular-builders/custom-webpack": "^11.0.0", 41 | "@angular-devkit/build-angular": "~0.1002.0", 42 | "@angular/cli": "~10.2.0", 43 | "@angular/compiler-cli": "~10.2.0", 44 | "@types/jasmine": "~3.5.0", 45 | "@types/jasminewd2": "~2.0.3", 46 | "@types/node": "^12.11.1", 47 | "angular-cli-ghpages": "^1.0.0-rc.1", 48 | "codelyzer": "^6.0.0", 49 | "jasmine-core": "~3.6.0", 50 | "jasmine-spec-reporter": "~5.0.0", 51 | "karma": "~5.0.0", 52 | "karma-chrome-launcher": "~3.1.0", 53 | "karma-coverage-istanbul-reporter": "~3.0.2", 54 | "karma-jasmine": "~4.0.0", 55 | "karma-jasmine-html-reporter": "^1.5.0", 56 | "protractor": "~7.0.0", 57 | "ts-node": "~8.3.0", 58 | "tslint": "~6.1.0", 59 | "typescript": "~4.0.2" 60 | }, 61 | "browser": { 62 | "fs": false, 63 | "os": false, 64 | "path": false 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ClaimComponent } from './claim/claim.component'; 4 | import { CreateComponent } from './create/create.component'; 5 | import { HomeComponent } from './home/home.component'; 6 | import { ProfileComponent } from './profile/profile.component'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | pathMatch: 'full', 12 | component: HomeComponent 13 | }, 14 | { 15 | path: 'create', 16 | component: CreateComponent 17 | }, 18 | { 19 | path: 'claim/:rootIPFSHash', 20 | component: ClaimComponent 21 | }, 22 | { 23 | path: 'profile/:userAddress', 24 | component: ProfileComponent 25 | } 26 | ]; 27 | 28 | @NgModule({ 29 | imports: [RouterModule.forRoot(routes)], 30 | exports: [RouterModule] 31 | }) 32 | export class AppRoutingModule { } 33 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'merkle-drop'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('merkle-drop'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('merkle-drop app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { WalletService } from './wallet.service'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.css'] 8 | }) 9 | export class AppComponent { 10 | title = 'merkle-drop'; 11 | 12 | constructor(public wallet: WalletService) { 13 | wallet.connect(() => {}, () => {}, true); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { MarkdownModule } from 'ngx-markdown'; 5 | 6 | import { AppRoutingModule } from './app-routing.module'; 7 | import { AppComponent } from './app.component'; 8 | import { CreateComponent } from './create/create.component'; 9 | import { ClaimComponent } from './claim/claim.component'; 10 | import { HomeComponent } from './home/home.component'; 11 | import { HeaderComponent } from './header/header.component'; 12 | import { ProfileComponent } from './profile/profile.component'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | AppComponent, 17 | CreateComponent, 18 | ClaimComponent, 19 | HomeComponent, 20 | HeaderComponent, 21 | ProfileComponent 22 | ], 23 | imports: [ 24 | BrowserModule, 25 | AppRoutingModule, 26 | FormsModule, 27 | MarkdownModule.forRoot() 28 | ], 29 | providers: [], 30 | bootstrap: [AppComponent] 31 | }) 32 | export class AppModule { } 33 | -------------------------------------------------------------------------------- /src/app/claim/claim.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/app/claim/claim.component.css -------------------------------------------------------------------------------- /src/app/claim/claim.component.html: -------------------------------------------------------------------------------- 1 | 2 | Loading... 3 | 4 | 5 | 6 | 7 |

{{remoteTree.metadata.name}}

8 | 9 | View 10 | smart contract on Etherscan 11 |
12 | View 13 | airdropped token on Etherscan 14 |
15 | Expires at {{expirationTime}} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |

Claimable amount: {{claimableAmount}} {{claimTokenSymbol}}

24 |
25 | 26 |

Claimable NFT ID: {{claimableAmount}} of {{claimTokenSymbol}}

27 |
28 | 29 |
30 | 31 |

32 | This address has already claimed from this airdrop. 33 |

34 | 35 | 36 |

37 | It appears that you are the owner of this airdrop, and the airdrop has expired. You can sweep the unclaimed tokens 38 | into your wallet. 39 |

40 | 41 |
42 | 43 | 44 |
-------------------------------------------------------------------------------- /src/app/claim/claim.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ClaimComponent } from './claim.component'; 4 | 5 | describe('ClaimComponent', () => { 6 | let component: ClaimComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ClaimComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ClaimComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/claim/claim.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { RemoteIPFSSearchTree } from '../../libs/ipfs-search-tree/remote-ipfs-search-tree'; 3 | import { ethers } from 'ethers'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import BigNumber from 'bignumber.js'; 6 | import { WalletService } from '../wallet.service'; 7 | import { ContractService } from '../contract.service'; 8 | 9 | @Component({ 10 | selector: 'app-claim', 11 | templateUrl: './claim.component.html', 12 | styleUrls: ['./claim.component.css'] 13 | }) 14 | export class ClaimComponent implements OnInit { 15 | IPFS_ENDPOINT = 'gateway.pinata.cloud'; 16 | rootIPFSHash: string; 17 | remoteTree: RemoteIPFSSearchTree; 18 | claimAddress: string; 19 | userClaim: any; 20 | airdropBalance: BigNumber; 21 | claimableAmount: string; 22 | claimTokenSymbol: string; 23 | claimed: boolean; 24 | finishedLoadingRoot: boolean; 25 | finishedCheckingClaim: boolean; 26 | expirationTime: string; 27 | sweepEnabled: boolean; 28 | 29 | constructor( 30 | private activatedRoute: ActivatedRoute, 31 | public wallet: WalletService, 32 | public contracts: ContractService 33 | ) { } 34 | 35 | async ngOnInit() { 36 | this.rootIPFSHash = this.activatedRoute.snapshot.paramMap.get('rootIPFSHash'); 37 | this.remoteTree = new RemoteIPFSSearchTree(this.IPFS_ENDPOINT, this.rootIPFSHash); 38 | await this.remoteTree.init(); 39 | 40 | const readonlyWeb3 = this.wallet.readonlyWeb3(); 41 | const astrodropContract = this.contracts.getContract(this.remoteTree.metadata.contractAddress, 'Astrodrop', readonlyWeb3); 42 | const expireTimestamp = (+await astrodropContract.methods.expireTimestamp().call()) * 1e3 43 | this.expirationTime = new Date(expireTimestamp).toString(); 44 | this.finishedLoadingRoot = true; 45 | 46 | const owner = await astrodropContract.methods.owner().call(); 47 | this.sweepEnabled = Date.now() >= expireTimestamp && this.wallet.userAddress.toLowerCase() === owner.toLowerCase(); 48 | } 49 | 50 | resetData() { 51 | this.claimed = false; 52 | this.finishedCheckingClaim = false; 53 | this.finishedLoadingRoot = false; 54 | } 55 | 56 | clickCheck() { 57 | if (!this.wallet.web3.utils.isAddress(this.claimAddress)) { 58 | this.wallet.displayGenericError(new Error('The provided address is not a valid Ethereum address.')); 59 | return; 60 | } 61 | 62 | this.checkClaim(this.claimAddress); 63 | } 64 | 65 | clickClaim() { 66 | this.claimAirdrop(this.claimAddress, this.userClaim); 67 | } 68 | 69 | clickSweep() { 70 | this.sweep(); 71 | } 72 | 73 | async getClaim(address: string) { 74 | const checksumAddress = ethers.utils.getAddress(address); 75 | const claim = await this.remoteTree.find(checksumAddress); 76 | return claim; 77 | } 78 | 79 | async checkClaim(claimAddress: string) { 80 | this.finishedCheckingClaim = false; 81 | 82 | const readonlyWeb3 = this.wallet.readonlyWeb3(); 83 | 84 | this.userClaim = await this.getClaim(claimAddress); 85 | if (!this.userClaim) { 86 | this.wallet.displayGenericError(new Error('The provided address is not included in this airdrop.')); 87 | return; 88 | } 89 | 90 | const astrodropContract = this.contracts.getContract(this.remoteTree.metadata.contractAddress, 'Astrodrop', readonlyWeb3); 91 | this.claimed = await astrodropContract.methods.isClaimed(this.userClaim.index).call(); 92 | 93 | const tokenAddress = this.remoteTree.metadata.tokenAddress; 94 | const tokenContract = this.contracts.getERC20(tokenAddress, readonlyWeb3); 95 | this.claimTokenSymbol = await tokenContract.methods.symbol().call(); 96 | 97 | if (!this.claimed) { 98 | let tokenDecimals; 99 | if (this.remoteTree.metadata.tokenType === '20') { 100 | tokenDecimals = +await tokenContract.methods.decimals().call(); 101 | } else if (this.remoteTree.metadata.tokenType === '721') { 102 | tokenDecimals = 0; 103 | } 104 | const tokenPrecision = new BigNumber(10).pow(tokenDecimals); 105 | this.airdropBalance = new BigNumber(this.userClaim.amount, 16).div(tokenPrecision); 106 | this.claimableAmount = this.airdropBalance.toFixed(tokenDecimals); 107 | } 108 | 109 | this.finishedCheckingClaim = true; 110 | } 111 | 112 | claimAirdrop(claimAddress: string, claim: any) { 113 | const astrodropContract = this.contracts.getContract(this.remoteTree.metadata.contractAddress, 'Astrodrop'); 114 | const func = astrodropContract.methods.claim(claim.index, claimAddress, claim.amount, claim.proof); 115 | 116 | this.wallet.sendTx(func, () => { }, () => { }, (error) => { this.wallet.displayGenericError(error) }); 117 | } 118 | 119 | sweep() { 120 | const astrodropContract = this.contracts.getContract(this.remoteTree.metadata.contractAddress, 'Astrodrop'); 121 | const func = astrodropContract.methods.sweep(this.remoteTree.metadata.tokenAddress, this.wallet.userAddress); 122 | 123 | this.wallet.sendTx(func, () => { }, () => { }, (error) => { this.wallet.displayGenericError(error) }); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/app/constants.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ConstantsService } from './constants.service'; 4 | 5 | describe('ConstantsService', () => { 6 | let service: ConstantsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ConstantsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/constants.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class ConstantsService { 7 | PRECISION = 1e18; 8 | GRAPHQL_ENDPOINT = 'https://api.thegraph.com/subgraphs/name/whalerdao/astrodrop'; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/contract.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ContractService } from './contract.service'; 4 | 5 | describe('ContractService', () => { 6 | let service: ContractService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ContractService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/contract.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import Web3 from 'web3'; 3 | import { WalletService } from './wallet.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class ContractService { 9 | 10 | constructor(public wallet: WalletService) { } 11 | 12 | getContract(address: string, abiName: string, web3?: Web3) { 13 | const abi = require(`../assets/abis/${abiName}.json`); 14 | if (web3) { 15 | return new web3.eth.Contract(abi, address); 16 | } 17 | return new this.wallet.web3.eth.Contract(abi, address); 18 | } 19 | 20 | getNamedContract(name: string, web3?: Web3) { 21 | const address = require('../assets/json/contracts.json')[name]; 22 | return this.getContract(address, name, web3); 23 | } 24 | 25 | getNamedContractAddress(name: string) { 26 | return require('../assets/json/contracts.json')[name]; 27 | } 28 | 29 | getERC20(address: string, web3?: Web3) { 30 | return this.getContract(address, 'ERC20', web3); 31 | } 32 | } -------------------------------------------------------------------------------- /src/app/create/create.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/app/create/create.component.css -------------------------------------------------------------------------------- /src/app/create/create.component.html: -------------------------------------------------------------------------------- 1 |

Create airdrop

2 | 3 | 4 |

Step 1/5: Specify the token to be airdropped

5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |

Step 2/5: Enter recipient addresses and airdrop amounts

21 | 22 | Format: 23 |
 24 | 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045,420.69
 25 | 0x46e01e73074937FEFb4104B5597Df32370172f86,1234.5678
 26 |     
27 | 28 |

29 | The airdrop amounts should be in decimal, and the number of decimals should not exceed the maximum supported by 30 | the 31 | airdropped token. 32 |

33 |
34 | 35 | 36 |

Step 2/5: Enter recipient addresses and airdrop token IDs

37 | 38 | Format: 39 |
 40 | 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045,2
 41 | 0x46e01e73074937FEFb4104B5597Df32370172f86,10
 42 |     
43 | 44 |

45 | Note: you can only airdrop a single NFT to each address. 46 |

47 |
48 | 49 | 50 | 51 | 52 | 53 |
 54 | Success!
 55 | Total airdrop amount: {{totalAirdropAmount}} {{tokenSymbol}}
 56 | Number of recipients: {{numRecipients}}
 57 |   
58 |
59 | 60 | 61 |

Step 3/5: Publish your Astrodrop page to IPFS

62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
79 | 80 |
81 | 82 |

Publishing... ({{uploadIPFSFilesPercentage * 100}}%)

83 | 84 |

Success!

85 |
86 | 87 | 88 |

Step 4/5: Deploy airdrop smart contract

89 | 90 | 91 | 92 | 93 |
94 | After the expiration time, you will be able to withdraw the unredeemed tokens. 95 | 96 |
97 | 98 |
99 |
100 | 101 | 102 |

Step 5/5: Last steps

103 | 104 |

105 | Your Astrodrop contract is being deployed at {{astrodropContractAddress}}. 107 |

108 | 109 | 110 |

111 | Important note: You need to send the tokens to be airdropped ({{totalAirdropAmount}} {{tokenSymbol}}) to 112 | your Astrodrop contract, so that the contract can distribute the tokens to the claimants. 113 |

114 |
115 | 116 | 117 |

118 | Important note: You need to give your Astrodrop contract approval to transfer your ERC-721 NFTs in order for the 119 | airdrop to be functional. 120 |

121 | 122 |

123 | 124 | If the NFTs are owned by a different account than the one you're using right now (e.g. a multi-signature 125 | wallet), please call setApprovalForAll(astrodropAddress) on the NFT contract using that account. 126 | 127 |

128 | 129 | 130 |
131 | 132 |

133 | Your Astrodrop page is available here. 134 |

135 |
136 | 137 |
138 | 139 |
-------------------------------------------------------------------------------- /src/app/create/create.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateComponent } from './create.component'; 4 | 5 | describe('CreateComponent', () => { 6 | let component: CreateComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CreateComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CreateComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/create/create.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { parseBalanceMap } from '../../libs/merkle-tree/parse-balance-map'; 3 | import { LocalIPFSSearchTree } from '../../libs/ipfs-search-tree/local-ipfs-search-tree'; 4 | import { WalletService } from '../wallet.service'; 5 | import { ContractService } from '../contract.service'; 6 | import { BigNumber } from 'bignumber.js'; 7 | import Base58 from 'base-58'; 8 | import { Metadata } from 'src/libs/ipfs-search-tree/interfaces'; 9 | import Papa from 'papaparse'; 10 | 11 | type BalanceFormat = { [account: string]: number | string } 12 | 13 | @Component({ 14 | selector: 'app-create', 15 | templateUrl: './create.component.html', 16 | styleUrls: ['./create.component.css'] 17 | }) 18 | export class CreateComponent implements OnInit { 19 | IPFS_ENDPOINT = 'api.thegraph.com/ipfs'; 20 | 21 | step: number; 22 | 23 | balancesInput: string; 24 | tokenTypeInput: string; 25 | tokenAddressInput: string; 26 | nameInput: string; 27 | descriptionInput: string; 28 | logoURLInput: string; 29 | expirationDateInput: string; 30 | expirationTimeInput: string; 31 | 32 | rootIPFSHash: string; 33 | merkleTree: any; 34 | astrodropContractAddress: string; 35 | tokenDecimals: number; 36 | totalAirdropAmount: string; 37 | tokenSymbol: string; 38 | numRecipients: number; 39 | salt: string; 40 | canContinue: boolean; 41 | 42 | uploadingIPFSFiles: boolean; 43 | uploadIPFSFilesPercentage: number; 44 | 45 | constructor( 46 | public wallet: WalletService, 47 | public contracts: ContractService 48 | ) { 49 | 50 | } 51 | 52 | ngOnInit(): void { 53 | this.balancesInput = ''; 54 | this.tokenTypeInput = '20'; 55 | this.tokenAddressInput = ''; 56 | this.nameInput = ''; 57 | this.descriptionInput = ''; 58 | this.logoURLInput = ''; 59 | this.expirationDateInput = ''; 60 | this.expirationTimeInput = ''; 61 | this.step = 1; 62 | this.canContinue = false; 63 | this.numRecipients = 0; 64 | this.uploadingIPFSFiles = false; 65 | this.uploadIPFSFilesPercentage = 0; 66 | } 67 | 68 | clickNext() { 69 | this.step += 1; 70 | this.canContinue = false; 71 | } 72 | 73 | async clickConfirmToken() { 74 | // check inputs 75 | if (!this.wallet.web3.utils.isAddress(this.tokenAddressInput)) { 76 | this.wallet.displayGenericError(new Error('Input not an Ethereum address')); 77 | return; 78 | } 79 | 80 | const tokenContract = this.contracts.getERC20(this.tokenAddressInput, this.wallet.readonlyWeb3()); 81 | if (this.tokenTypeInput === '20') { 82 | await Promise.all([ 83 | tokenContract.methods.decimals().call().then(decimals => this.tokenDecimals = +decimals), 84 | tokenContract.methods.symbol().call().then(symbol => this.tokenSymbol = symbol) 85 | ]); 86 | } else if (this.tokenTypeInput === '721') { 87 | await tokenContract.methods.symbol().call().then(symbol => this.tokenSymbol = symbol); 88 | this.tokenDecimals = 0; 89 | } 90 | 91 | this.step += 1; 92 | } 93 | 94 | async clickParseBalances() { 95 | try { 96 | this.parseBalances(this.balancesInput); 97 | this.numRecipients = Object.keys(this.merkleTree.claims).length; 98 | if (this.tokenTypeInput === '20') { 99 | this.totalAirdropAmount = new BigNumber(this.merkleTree.tokenTotal, 16).div(new BigNumber(10).pow(this.tokenDecimals)).toFixed(this.tokenDecimals); 100 | } else if (this.tokenTypeInput === '721') { 101 | this.totalAirdropAmount = new BigNumber(this.numRecipients).toFixed(); 102 | } 103 | const unixTimestamp = Date.now(); 104 | this.salt = '0x' + new BigNumber(this.merkleTree.merkleRoot, 16).plus(unixTimestamp).toString(16); 105 | this.astrodropContractAddress = await this.computeAstrodropAddress(this.salt); 106 | this.canContinue = true; 107 | } catch (error) { 108 | this.wallet.displayGenericError(error); 109 | } 110 | } 111 | 112 | async clickDeploy() { 113 | const expirationTimestamp = Math.floor(Date.parse(`${this.expirationDateInput} ${this.expirationTimeInput}`) / 1e3); 114 | this.deployAstrodropContract(this.tokenAddressInput, this.merkleTree.merkleRoot, expirationTimestamp, this.salt, this.rootIPFSHash); 115 | } 116 | 117 | async clickUpload() { 118 | const metadata: Metadata = { 119 | name: this.nameInput, 120 | description: this.descriptionInput, 121 | logoURL: this.logoURLInput, 122 | contractAddress: this.astrodropContractAddress, 123 | merkleRoot: this.merkleTree.merkleRoot, 124 | tokenAddress: this.tokenAddressInput, 125 | tokenTotal: this.merkleTree.tokenTotal, 126 | tokenType: this.tokenTypeInput 127 | }; 128 | this.uploadingIPFSFiles = true; 129 | const updateProgress = (percentageChange) => { 130 | this.uploadIPFSFilesPercentage += percentageChange; 131 | } 132 | await this.uploadTree(this.merkleTree, metadata, updateProgress); 133 | this.canContinue = true; 134 | this.uploadingIPFSFiles = false; 135 | } 136 | 137 | clickSetApprovalForAll() { 138 | this.setApprovalForAll(this.tokenAddressInput, this.astrodropContractAddress); 139 | } 140 | 141 | private parseBalances(rawBalances: string) { 142 | // parse CSV balances to JSON 143 | const parseResults = Papa.parse(rawBalances); 144 | const balances: BalanceFormat = {}; 145 | if (parseResults.errors.length > 0) { 146 | throw parseResults.errors[0]; 147 | } 148 | for (const row of parseResults.data) { 149 | if (row.length != 2) { 150 | throw new Error(`Invalid row: ${row}`); 151 | } 152 | const claimant = row[0]; 153 | const balance = row[1]; 154 | balances[claimant] = new BigNumber(balance).times(new BigNumber(10).pow(this.tokenDecimals)).integerValue().toString(16); 155 | } 156 | 157 | // create merkle tree 158 | this.merkleTree = parseBalanceMap(balances); 159 | } 160 | 161 | private async uploadTree(merkleTree: any, metadata: any, updateProgress: any) { 162 | // create search tree 163 | const searchTree = new LocalIPFSSearchTree(this.IPFS_ENDPOINT, merkleTree.claims, metadata, updateProgress); 164 | 165 | // upload search tree to IPFS 166 | this.rootIPFSHash = await searchTree.uploadData(); 167 | } 168 | 169 | private computeAstrodropAddress(salt: string): Promise { 170 | const astrodropFactoryContract = this.contracts.getNamedContract('AstrodropFactory'); 171 | let astrodropTemplateAddress; 172 | if (this.tokenTypeInput === '20') { 173 | astrodropTemplateAddress = this.contracts.getNamedContractAddress('Astrodrop'); 174 | } else if (this.tokenTypeInput === '721') { 175 | astrodropTemplateAddress = this.contracts.getNamedContractAddress('AstrodropERC721'); 176 | } 177 | return astrodropFactoryContract.methods.computeAstrodropAddress(astrodropTemplateAddress, salt).call(); 178 | } 179 | 180 | private deployAstrodropContract(tokenAddress: string, merkleRoot: string, expireTimestamp: number, salt: string, ipfsHash: string) { 181 | // convert ipfsHash to 32 bytes by removing the first two bytes 182 | const truncatedIPFSHash = this.wallet.web3.utils.bytesToHex(Base58.decode(ipfsHash).slice(2)); 183 | 184 | const astrodropFactoryContract = this.contracts.getNamedContract('AstrodropFactory'); 185 | let astrodropTemplateAddress; 186 | if (this.tokenTypeInput === '20') { 187 | astrodropTemplateAddress = this.contracts.getNamedContractAddress('Astrodrop'); 188 | } else if (this.tokenTypeInput === '721') { 189 | astrodropTemplateAddress = this.contracts.getNamedContractAddress('AstrodropERC721'); 190 | } 191 | const func = astrodropFactoryContract.methods.createAstrodrop( 192 | astrodropTemplateAddress, 193 | tokenAddress, 194 | merkleRoot, 195 | expireTimestamp, 196 | salt, 197 | truncatedIPFSHash 198 | ); 199 | return this.wallet.sendTx(func, () => { this.canContinue = true; }, () => { }, (error) => { this.wallet.displayGenericError(error) }); 200 | } 201 | 202 | setApprovalForAll(tokenAddress: string, astrodropAddress: string) { 203 | const tokenContract = this.contracts.getContract(tokenAddress, 'ERC721'); 204 | const func = tokenContract.methods.setApprovalForAll(astrodropAddress, true); 205 | 206 | return this.wallet.sendTx(func, () => { }, () => { }, (error) => { this.wallet.displayGenericError(error) }); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/app/header/header.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/app/header/header.component.css -------------------------------------------------------------------------------- /src/app/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Astrodrop

4 |
5 | 6 |
7 | 8 | My profile 9 | 10 | 18 |
19 |
-------------------------------------------------------------------------------- /src/app/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HeaderComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HeaderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { WalletService } from '../wallet.service'; 3 | 4 | @Component({ 5 | selector: 'app-header', 6 | templateUrl: './header.component.html', 7 | styleUrls: ['./header.component.css'] 8 | }) 9 | export class HeaderComponent implements OnInit { 10 | 11 | constructor(public wallet: WalletService) { } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | connectWallet() { 17 | this.wallet.connect(() => { }, () => { }, false); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/helpers.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { HelpersService } from './helpers.service'; 4 | 5 | describe('HelpersService', () => { 6 | let service: HelpersService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(HelpersService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/helpers.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import BigNumber from 'bignumber.js'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class HelpersService { 8 | 9 | constructor() { } 10 | 11 | processWeb3Number(number): string { 12 | return new BigNumber(number).integerValue().toFixed(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/home/home.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/app/home/home.component.css -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

Token airdrops for 212.5k gas

2 | 3 |

4 | Astrodrop uses Merkle trees to enable airdropping tokens to any number of accounts for the same cost: 212.5k gas. 5 |

6 | 7 |

8 | Astrodrop also allows users to view the list of astrodrops they are eligible for, so that 9 | you'll never miss a high-value airdrop again. 10 |

11 | 12 |

13 | Each astrodrop also comes with a customizable claim page, where users can claim their airdropped tokens and read more 14 | about the airdrop. 15 |

16 | 17 | -------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | templateUrl: './home.component.html', 6 | styleUrls: ['./home.component.css'] 7 | }) 8 | export class HomeComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/profile/profile.component.css: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | display: grid; 3 | grid-template-columns: auto auto auto; 4 | } 5 | 6 | .card-container { 7 | display: grid; 8 | } -------------------------------------------------------------------------------- /src/app/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |

My profile

2 | 3 |

Eligible Astrodrops

4 | 5 |
6 |
7 | 8 | {{drop.name}} 9 |
10 |
11 | 12 |

My Astrodrops

13 | 14 |
15 |
16 | 17 | {{drop.name}} 18 |
19 |
-------------------------------------------------------------------------------- /src/app/profile/profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProfileComponent } from './profile.component'; 4 | 5 | describe('ProfileComponent', () => { 6 | let component: ProfileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProfileComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProfileComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import BigNumber from 'bignumber.js'; 4 | import { request, gql } from 'graphql-request' 5 | import { ConstantsService } from '../constants.service'; 6 | import { ContractService } from '../contract.service'; 7 | import { WalletService } from '../wallet.service'; 8 | 9 | @Component({ 10 | selector: 'app-profile', 11 | templateUrl: './profile.component.html', 12 | styleUrls: ['./profile.component.css'] 13 | }) 14 | export class ProfileComponent implements OnInit { 15 | userAddress: string; 16 | eligibleAstrodrops: Astrodrop[]; 17 | createdAstrodrops: Astrodrop[]; 18 | 19 | constructor( 20 | private activatedRoute: ActivatedRoute, 21 | public wallet: WalletService, 22 | public constants: ConstantsService, 23 | public contracts: ContractService 24 | ) { 25 | this.eligibleAstrodrops = []; 26 | this.createdAstrodrops = []; 27 | } 28 | 29 | ngOnInit(): void { 30 | this.userAddress = this.activatedRoute.snapshot.paramMap.get('userAddress'); 31 | this.loadData(); 32 | } 33 | 34 | loadData() { 35 | const claimantID = this.wallet.web3.utils.toChecksumAddress(this.userAddress); 36 | const creatorID = this.userAddress.toLowerCase(); 37 | const queryString = gql` 38 | { 39 | claimant(id: "${claimantID}") { 40 | eligibleAstrodrops { 41 | astrodrop { 42 | id 43 | name 44 | logoURL 45 | ipfsHash 46 | } 47 | } 48 | } 49 | astrodrops(where: { creator: "${creatorID}" }) { 50 | id 51 | name 52 | logoURL 53 | ipfsHash 54 | } 55 | } 56 | `; 57 | request(this.constants.GRAPHQL_ENDPOINT, queryString).then((data) => this.handleData(data)); 58 | } 59 | 60 | async handleData(queryResult: QueryResult) { 61 | const claimant = queryResult.claimant; 62 | const createdAstrodrops = queryResult.astrodrops; 63 | 64 | if (claimant) { 65 | const rawDrops = claimant.eligibleAstrodrops; 66 | const parsedDrops: Astrodrop[] = []; 67 | for (const rawDrop of rawDrops) { 68 | parsedDrops.push({ 69 | name: rawDrop.astrodrop.name, 70 | logoURL: rawDrop.astrodrop.logoURL, 71 | ipfsHash: rawDrop.astrodrop.ipfsHash 72 | }); 73 | } 74 | this.eligibleAstrodrops = parsedDrops; 75 | } 76 | 77 | if (createdAstrodrops) { 78 | const rawDrops = createdAstrodrops; 79 | const parsedDrops: Astrodrop[] = []; 80 | for (const rawDrop of rawDrops) { 81 | parsedDrops.push({ 82 | name: rawDrop.name, 83 | logoURL: rawDrop.logoURL, 84 | ipfsHash: rawDrop.ipfsHash 85 | }); 86 | } 87 | this.createdAstrodrops = parsedDrops; 88 | } 89 | } 90 | } 91 | 92 | interface QueryResult { 93 | claimant: { 94 | eligibleAstrodrops: { 95 | astrodrop: { 96 | id: string 97 | name: string | null 98 | logoURL: string | null 99 | ipfsHash: string 100 | } 101 | }[]; 102 | }; 103 | astrodrops: { 104 | id: string 105 | name: string | null 106 | logoURL: string | null 107 | ipfsHash: string 108 | }[]; 109 | } 110 | 111 | interface Astrodrop { 112 | name: string; 113 | logoURL: string; 114 | ipfsHash: string; 115 | } -------------------------------------------------------------------------------- /src/app/wallet.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { WalletService } from './wallet.service'; 4 | 5 | describe('WalletService', () => { 6 | let service: WalletService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(WalletService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/wallet.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, EventEmitter } from '@angular/core'; 2 | import { Web3Enabled } from './web3Enabled'; 3 | import Web3 from 'web3'; 4 | import { WEB3 } from './web3'; 5 | import { isNullOrUndefined } from 'util'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class WalletService extends Web3Enabled { 11 | connectedEvent: EventEmitter; 12 | disconnectedEvent: EventEmitter; 13 | 14 | constructor(@Inject(WEB3) public web3: Web3) { 15 | super(web3); 16 | this.connectedEvent = new EventEmitter(); 17 | this.disconnectedEvent = new EventEmitter(); 18 | } 19 | 20 | public get userAddress(): string { 21 | return this.state.address; 22 | } 23 | 24 | public get connected(): boolean { 25 | return !isNullOrUndefined(this.state.address); 26 | } 27 | 28 | async connect(onConnected, onError, isStartupMode: boolean) { 29 | const _onConnected = () => { 30 | this.connectedEvent.emit(); 31 | onConnected(); 32 | }; 33 | const _onError = () => { 34 | this.disconnectedEvent.emit(); 35 | onError(); 36 | } 37 | await super.connect(_onConnected, _onError, isStartupMode); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/web3.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import Web3 from 'web3'; 3 | 4 | export const WEB3 = new InjectionToken('web3', { 5 | providedIn: 'root', 6 | factory: () => { 7 | try { 8 | const provider = ('ethereum' in window) ? window['ethereum'] : Web3.givenProvider; 9 | return new Web3(provider); 10 | } catch (err) { 11 | throw new Error('Non-Ethereum browser detected. You should consider trying MetaMask!'); 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/web3Enabled.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import Notify from 'bnc-notify'; 3 | import Onboard from 'bnc-onboard'; 4 | import BigNumber from 'bignumber.js'; 5 | 6 | export class Web3Enabled { 7 | blocknativeAPIKey: string; 8 | infuraKey: string; 9 | infuraEndpoint: string; 10 | assistInstance: any; 11 | notifyInstance: any; 12 | state: any; 13 | networkID: number; 14 | 15 | constructor(public web3: Web3) { 16 | this.blocknativeAPIKey = '08eaf62d-228c-4ec6-a033-f8b97689102b'; 17 | this.infuraKey = '7a7dd3472294438eab040845d03c215c'; 18 | this.infuraEndpoint = `https://mainnet.infura.io/v3/${this.infuraKey}` 19 | this.networkID = 1; 20 | this.state = { 21 | address: null, 22 | wallet: { 23 | provider: null 24 | } 25 | }; 26 | } 27 | 28 | async connect(onConnected, onError, isStartupMode: boolean) { 29 | if (!this.assistInstance) { 30 | const wallets = [ 31 | { 32 | walletName: 'metamask', 33 | preferred: true 34 | }, 35 | { 36 | walletName: 'walletConnect', 37 | infuraKey: this.infuraKey, 38 | networkId: this.networkID, 39 | preferred: true 40 | } 41 | ]; 42 | 43 | const walletChecks = [ 44 | { checkName: 'derivationPath' }, 45 | { checkName: 'connect' }, 46 | { checkName: 'accounts' }, 47 | { checkName: 'network' }, 48 | ]; 49 | 50 | const walletSelectConfig = { 51 | heading: 'Select a Wallet', 52 | description: 'Please select a wallet to connect to Astrodrop:', 53 | wallets 54 | }; 55 | 56 | const bncAssistConfig = { 57 | dappId: this.blocknativeAPIKey, 58 | darkMode: true, 59 | networkId: this.networkID, 60 | hideBranding: true, 61 | subscriptions: { 62 | wallet: wallet => { 63 | if (wallet.provider) { 64 | this.web3 = new Web3(wallet.provider); 65 | } 66 | // store the selected wallet name to be retrieved next time the app loads 67 | window.localStorage.setItem('selectedWallet', wallet.name); 68 | }, 69 | address: this.doNothing, 70 | network: this.doNothing, 71 | balance: this.doNothing 72 | }, 73 | walletSelect: walletSelectConfig, 74 | walletCheck: walletChecks 75 | }; 76 | this.assistInstance = Onboard(bncAssistConfig); 77 | } 78 | 79 | // Get user to select a wallet 80 | let selectedWallet; 81 | if (isStartupMode) { 82 | // Startup mode: connect to previously used wallet if available 83 | // get the selectedWallet value from local storage 84 | const previouslySelectedWallet = window.localStorage.getItem('selectedWallet'); 85 | // call wallet select with that value if it exists 86 | if (previouslySelectedWallet != null) { 87 | selectedWallet = await this.assistInstance.walletSelect(previouslySelectedWallet); 88 | } 89 | } else { 90 | // Non startup mode: open wallet selection screen 91 | selectedWallet = await this.assistInstance.walletSelect(); 92 | } 93 | const state = this.assistInstance.getState(); 94 | if ( 95 | selectedWallet 96 | || state.address !== null // If user already logged in but want to switch account, and then dismissed window 97 | ) { 98 | // Get users' wallet ready to transact 99 | const ready = await this.assistInstance.walletCheck(); 100 | this.state = this.assistInstance.getState(); 101 | 102 | if (!ready) { 103 | // Selected an option but then dismissed it 104 | // Treat as no wallet 105 | onError(); 106 | } else { 107 | // Successfully connected 108 | onConnected(); 109 | } 110 | } else { 111 | // User refuses to connect to wallet 112 | // Update state 113 | this.state = this.assistInstance.getState(); 114 | onError(); 115 | } 116 | 117 | if (!this.notifyInstance) { 118 | this.notifyInstance = Notify({ 119 | dappId: this.blocknativeAPIKey, 120 | networkId: this.networkID 121 | }); 122 | this.notifyInstance.config({ 123 | darkMode: true 124 | }); 125 | } 126 | } 127 | 128 | readonlyWeb3() { 129 | if (this.state.wallet.provider) { 130 | return this.web3; 131 | } 132 | return new Web3(this.infuraEndpoint); 133 | } 134 | 135 | async estimateGas(func, val, _onError) { 136 | return Math.floor((await func.estimateGas({ 137 | from: this.state.address, 138 | value: val 139 | }).catch(_onError)) * 1.2); 140 | } 141 | 142 | async sendTx(func, _onTxHash, _onReceipt, _onError) { 143 | const gasLimit = await this.estimateGas(func, 0, _onError); 144 | if (!isNaN(gasLimit)) { 145 | return func.send({ 146 | from: this.state.address, 147 | gas: gasLimit, 148 | }).on('transactionHash', (hash) => { 149 | _onTxHash(hash); 150 | const { emitter } = this.notifyInstance.hash(hash); 151 | // emitter.on('txConfirmed', _onReceipt); 152 | emitter.on('txFailed', _onError); 153 | }) 154 | .on('receipt', _onReceipt) 155 | .on('error', (e) => { 156 | if (!e.toString().contains('newBlockHeaders')) { 157 | _onError(e); 158 | } 159 | }); 160 | } 161 | } 162 | 163 | async sendTxWithValue(func, val, _onTxHash, _onReceipt, _onError) { 164 | const gasLimit = await this.estimateGas(func, val, _onError); 165 | if (!isNaN(gasLimit)) { 166 | return func.send({ 167 | from: this.state.address, 168 | gas: gasLimit, 169 | value: val 170 | }).on('transactionHash', (hash) => { 171 | _onTxHash(hash); 172 | const { emitter } = this.notifyInstance.hash(hash); 173 | // emitter.on('txConfirmed', _onReceipt); 174 | emitter.on('txFailed', _onError); 175 | }) 176 | .on('receipt', _onReceipt) 177 | .on('error', (e) => { 178 | if (!e.toString().contains('newBlockHeaders')) { 179 | _onError(e); 180 | } 181 | }); 182 | } 183 | } 184 | 185 | async sendTxWithToken(func, token, to, amount, _onTxHash, _onReceipt, _onError) { 186 | const maxAllowance = new BigNumber(2).pow(256).minus(1).integerValue().toFixed(); 187 | const allowance = new BigNumber(await token.methods.allowance(this.state.address, to).call()); 188 | if (allowance.gte(amount)) { 189 | return this.sendTx(func, _onTxHash, _onReceipt, _onError); 190 | } 191 | return this.sendTx(token.methods.approve(to, maxAllowance), this.doNothing, () => { 192 | this.sendTx(func, _onTxHash, _onReceipt, _onError); 193 | }, _onError); 194 | } 195 | 196 | displayGenericError(error: Error) { 197 | let errorMessage; 198 | try { 199 | errorMessage = JSON.parse(error.message.slice(error.message.indexOf('{'))).originalError.message; 200 | } catch (err) { 201 | errorMessage = error.message; 202 | } 203 | this.notifyInstance.notification({ 204 | eventCode: 'genericError', 205 | type: 'error', 206 | message: errorMessage 207 | }); 208 | } 209 | 210 | doNothing() { } 211 | } 212 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/abis/Astrodrop.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "index", 7 | "type": "uint256" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "account", 12 | "type": "address" 13 | }, 14 | { 15 | "internalType": "uint256", 16 | "name": "amount", 17 | "type": "uint256" 18 | }, 19 | { 20 | "internalType": "bytes32[]", 21 | "name": "merkleProof", 22 | "type": "bytes32[]" 23 | } 24 | ], 25 | "name": "claim", 26 | "outputs": [], 27 | "stateMutability": "nonpayable", 28 | "type": "function" 29 | }, 30 | { 31 | "anonymous": false, 32 | "inputs": [ 33 | { 34 | "indexed": false, 35 | "internalType": "uint256", 36 | "name": "index", 37 | "type": "uint256" 38 | }, 39 | { 40 | "indexed": false, 41 | "internalType": "address", 42 | "name": "account", 43 | "type": "address" 44 | }, 45 | { 46 | "indexed": false, 47 | "internalType": "uint256", 48 | "name": "amount", 49 | "type": "uint256" 50 | } 51 | ], 52 | "name": "Claimed", 53 | "type": "event" 54 | }, 55 | { 56 | "inputs": [ 57 | { 58 | "internalType": "address", 59 | "name": "owner_", 60 | "type": "address" 61 | }, 62 | { 63 | "internalType": "address", 64 | "name": "token_", 65 | "type": "address" 66 | }, 67 | { 68 | "internalType": "bytes32", 69 | "name": "merkleRoot_", 70 | "type": "bytes32" 71 | }, 72 | { 73 | "internalType": "uint256", 74 | "name": "expireTimestamp_", 75 | "type": "uint256" 76 | } 77 | ], 78 | "name": "init", 79 | "outputs": [], 80 | "stateMutability": "nonpayable", 81 | "type": "function" 82 | }, 83 | { 84 | "anonymous": false, 85 | "inputs": [ 86 | { 87 | "indexed": true, 88 | "internalType": "address", 89 | "name": "previousOwner", 90 | "type": "address" 91 | }, 92 | { 93 | "indexed": true, 94 | "internalType": "address", 95 | "name": "newOwner", 96 | "type": "address" 97 | } 98 | ], 99 | "name": "OwnershipTransferred", 100 | "type": "event" 101 | }, 102 | { 103 | "inputs": [], 104 | "name": "renounceOwnership", 105 | "outputs": [], 106 | "stateMutability": "nonpayable", 107 | "type": "function" 108 | }, 109 | { 110 | "inputs": [ 111 | { 112 | "internalType": "address", 113 | "name": "token_", 114 | "type": "address" 115 | }, 116 | { 117 | "internalType": "address", 118 | "name": "target", 119 | "type": "address" 120 | } 121 | ], 122 | "name": "sweep", 123 | "outputs": [], 124 | "stateMutability": "nonpayable", 125 | "type": "function" 126 | }, 127 | { 128 | "inputs": [ 129 | { 130 | "internalType": "address", 131 | "name": "newOwner", 132 | "type": "address" 133 | } 134 | ], 135 | "name": "transferOwnership", 136 | "outputs": [], 137 | "stateMutability": "nonpayable", 138 | "type": "function" 139 | }, 140 | { 141 | "inputs": [ 142 | { 143 | "internalType": "uint256", 144 | "name": "", 145 | "type": "uint256" 146 | } 147 | ], 148 | "name": "claimedBitMap", 149 | "outputs": [ 150 | { 151 | "internalType": "uint256", 152 | "name": "", 153 | "type": "uint256" 154 | } 155 | ], 156 | "stateMutability": "view", 157 | "type": "function" 158 | }, 159 | { 160 | "inputs": [], 161 | "name": "expireTimestamp", 162 | "outputs": [ 163 | { 164 | "internalType": "uint256", 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "stateMutability": "view", 170 | "type": "function" 171 | }, 172 | { 173 | "inputs": [], 174 | "name": "initialized", 175 | "outputs": [ 176 | { 177 | "internalType": "bool", 178 | "name": "", 179 | "type": "bool" 180 | } 181 | ], 182 | "stateMutability": "view", 183 | "type": "function" 184 | }, 185 | { 186 | "inputs": [ 187 | { 188 | "internalType": "uint256", 189 | "name": "index", 190 | "type": "uint256" 191 | } 192 | ], 193 | "name": "isClaimed", 194 | "outputs": [ 195 | { 196 | "internalType": "bool", 197 | "name": "", 198 | "type": "bool" 199 | } 200 | ], 201 | "stateMutability": "view", 202 | "type": "function" 203 | }, 204 | { 205 | "inputs": [], 206 | "name": "isOwner", 207 | "outputs": [ 208 | { 209 | "internalType": "bool", 210 | "name": "", 211 | "type": "bool" 212 | } 213 | ], 214 | "stateMutability": "view", 215 | "type": "function" 216 | }, 217 | { 218 | "inputs": [], 219 | "name": "merkleRoot", 220 | "outputs": [ 221 | { 222 | "internalType": "bytes32", 223 | "name": "", 224 | "type": "bytes32" 225 | } 226 | ], 227 | "stateMutability": "view", 228 | "type": "function" 229 | }, 230 | { 231 | "inputs": [], 232 | "name": "owner", 233 | "outputs": [ 234 | { 235 | "internalType": "address", 236 | "name": "", 237 | "type": "address" 238 | } 239 | ], 240 | "stateMutability": "view", 241 | "type": "function" 242 | }, 243 | { 244 | "inputs": [], 245 | "name": "token", 246 | "outputs": [ 247 | { 248 | "internalType": "address", 249 | "name": "", 250 | "type": "address" 251 | } 252 | ], 253 | "stateMutability": "view", 254 | "type": "function" 255 | } 256 | ] -------------------------------------------------------------------------------- /src/assets/abis/AstrodropFactory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "template", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "token", 12 | "type": "address" 13 | }, 14 | { 15 | "internalType": "bytes32", 16 | "name": "merkleRoot", 17 | "type": "bytes32" 18 | }, 19 | { 20 | "internalType": "uint256", 21 | "name": "expireTimestamp", 22 | "type": "uint256" 23 | }, 24 | { 25 | "internalType": "bytes32", 26 | "name": "salt", 27 | "type": "bytes32" 28 | }, 29 | { 30 | "internalType": "bytes32", 31 | "name": "ipfsHash", 32 | "type": "bytes32" 33 | } 34 | ], 35 | "name": "createAstrodrop", 36 | "outputs": [ 37 | { 38 | "internalType": "contract Astrodrop", 39 | "name": "drop", 40 | "type": "address" 41 | } 42 | ], 43 | "stateMutability": "nonpayable", 44 | "type": "function" 45 | }, 46 | { 47 | "anonymous": false, 48 | "inputs": [ 49 | { 50 | "indexed": false, 51 | "internalType": "address", 52 | "name": "astrodrop", 53 | "type": "address" 54 | }, 55 | { 56 | "indexed": false, 57 | "internalType": "bytes32", 58 | "name": "ipfsHash", 59 | "type": "bytes32" 60 | } 61 | ], 62 | "name": "CreateAstrodrop", 63 | "type": "event" 64 | }, 65 | { 66 | "inputs": [ 67 | { 68 | "internalType": "address", 69 | "name": "template", 70 | "type": "address" 71 | }, 72 | { 73 | "internalType": "bytes32", 74 | "name": "salt", 75 | "type": "bytes32" 76 | } 77 | ], 78 | "name": "computeAstrodropAddress", 79 | "outputs": [ 80 | { 81 | "internalType": "address", 82 | "name": "", 83 | "type": "address" 84 | } 85 | ], 86 | "stateMutability": "view", 87 | "type": "function" 88 | }, 89 | { 90 | "inputs": [ 91 | { 92 | "internalType": "address", 93 | "name": "template", 94 | "type": "address" 95 | }, 96 | { 97 | "internalType": "address", 98 | "name": "query", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "isAstrodrop", 103 | "outputs": [ 104 | { 105 | "internalType": "bool", 106 | "name": "", 107 | "type": "bool" 108 | } 109 | ], 110 | "stateMutability": "view", 111 | "type": "function" 112 | } 113 | ] -------------------------------------------------------------------------------- /src/assets/abis/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "string", 6 | "name": "name", 7 | "type": "string" 8 | }, 9 | { 10 | "internalType": "string", 11 | "name": "symbol", 12 | "type": "string" 13 | }, 14 | { 15 | "internalType": "uint8", 16 | "name": "decimals", 17 | "type": "uint8" 18 | } 19 | ], 20 | "payable": false, 21 | "stateMutability": "nonpayable", 22 | "type": "constructor" 23 | }, 24 | { 25 | "anonymous": false, 26 | "inputs": [ 27 | { 28 | "indexed": true, 29 | "internalType": "address", 30 | "name": "owner", 31 | "type": "address" 32 | }, 33 | { 34 | "indexed": true, 35 | "internalType": "address", 36 | "name": "spender", 37 | "type": "address" 38 | }, 39 | { 40 | "indexed": false, 41 | "internalType": "uint256", 42 | "name": "value", 43 | "type": "uint256" 44 | } 45 | ], 46 | "name": "Approval", 47 | "type": "event" 48 | }, 49 | { 50 | "anonymous": false, 51 | "inputs": [ 52 | { 53 | "indexed": true, 54 | "internalType": "address", 55 | "name": "from", 56 | "type": "address" 57 | }, 58 | { 59 | "indexed": true, 60 | "internalType": "address", 61 | "name": "to", 62 | "type": "address" 63 | }, 64 | { 65 | "indexed": false, 66 | "internalType": "uint256", 67 | "name": "value", 68 | "type": "uint256" 69 | } 70 | ], 71 | "name": "Transfer", 72 | "type": "event" 73 | }, 74 | { 75 | "constant": true, 76 | "inputs": [ 77 | { 78 | "internalType": "address", 79 | "name": "owner", 80 | "type": "address" 81 | }, 82 | { 83 | "internalType": "address", 84 | "name": "spender", 85 | "type": "address" 86 | } 87 | ], 88 | "name": "allowance", 89 | "outputs": [ 90 | { 91 | "internalType": "uint256", 92 | "name": "", 93 | "type": "uint256" 94 | } 95 | ], 96 | "payable": false, 97 | "stateMutability": "view", 98 | "type": "function" 99 | }, 100 | { 101 | "constant": false, 102 | "inputs": [ 103 | { 104 | "internalType": "address", 105 | "name": "spender", 106 | "type": "address" 107 | }, 108 | { 109 | "internalType": "uint256", 110 | "name": "amount", 111 | "type": "uint256" 112 | } 113 | ], 114 | "name": "approve", 115 | "outputs": [ 116 | { 117 | "internalType": "bool", 118 | "name": "", 119 | "type": "bool" 120 | } 121 | ], 122 | "payable": false, 123 | "stateMutability": "nonpayable", 124 | "type": "function" 125 | }, 126 | { 127 | "constant": true, 128 | "inputs": [ 129 | { 130 | "internalType": "address", 131 | "name": "account", 132 | "type": "address" 133 | } 134 | ], 135 | "name": "balanceOf", 136 | "outputs": [ 137 | { 138 | "internalType": "uint256", 139 | "name": "", 140 | "type": "uint256" 141 | } 142 | ], 143 | "payable": false, 144 | "stateMutability": "view", 145 | "type": "function" 146 | }, 147 | { 148 | "constant": true, 149 | "inputs": [], 150 | "name": "decimals", 151 | "outputs": [ 152 | { 153 | "internalType": "uint8", 154 | "name": "", 155 | "type": "uint8" 156 | } 157 | ], 158 | "payable": false, 159 | "stateMutability": "view", 160 | "type": "function" 161 | }, 162 | { 163 | "constant": true, 164 | "inputs": [], 165 | "name": "name", 166 | "outputs": [ 167 | { 168 | "internalType": "string", 169 | "name": "", 170 | "type": "string" 171 | } 172 | ], 173 | "payable": false, 174 | "stateMutability": "view", 175 | "type": "function" 176 | }, 177 | { 178 | "constant": true, 179 | "inputs": [], 180 | "name": "symbol", 181 | "outputs": [ 182 | { 183 | "internalType": "string", 184 | "name": "", 185 | "type": "string" 186 | } 187 | ], 188 | "payable": false, 189 | "stateMutability": "view", 190 | "type": "function" 191 | }, 192 | { 193 | "constant": true, 194 | "inputs": [], 195 | "name": "totalSupply", 196 | "outputs": [ 197 | { 198 | "internalType": "uint256", 199 | "name": "", 200 | "type": "uint256" 201 | } 202 | ], 203 | "payable": false, 204 | "stateMutability": "view", 205 | "type": "function" 206 | }, 207 | { 208 | "constant": false, 209 | "inputs": [ 210 | { 211 | "internalType": "address", 212 | "name": "recipient", 213 | "type": "address" 214 | }, 215 | { 216 | "internalType": "uint256", 217 | "name": "amount", 218 | "type": "uint256" 219 | } 220 | ], 221 | "name": "transfer", 222 | "outputs": [ 223 | { 224 | "internalType": "bool", 225 | "name": "", 226 | "type": "bool" 227 | } 228 | ], 229 | "payable": false, 230 | "stateMutability": "nonpayable", 231 | "type": "function" 232 | }, 233 | { 234 | "constant": false, 235 | "inputs": [ 236 | { 237 | "internalType": "address", 238 | "name": "sender", 239 | "type": "address" 240 | }, 241 | { 242 | "internalType": "address", 243 | "name": "recipient", 244 | "type": "address" 245 | }, 246 | { 247 | "internalType": "uint256", 248 | "name": "amount", 249 | "type": "uint256" 250 | } 251 | ], 252 | "name": "transferFrom", 253 | "outputs": [ 254 | { 255 | "internalType": "bool", 256 | "name": "", 257 | "type": "bool" 258 | } 259 | ], 260 | "payable": false, 261 | "stateMutability": "nonpayable", 262 | "type": "function" 263 | } 264 | ] -------------------------------------------------------------------------------- /src/assets/abis/ERC721.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "owner", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "approved", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": true, 19 | "internalType": "uint256", 20 | "name": "tokenId", 21 | "type": "uint256" 22 | } 23 | ], 24 | "name": "Approval", 25 | "type": "event" 26 | }, 27 | { 28 | "anonymous": false, 29 | "inputs": [ 30 | { 31 | "indexed": true, 32 | "internalType": "address", 33 | "name": "owner", 34 | "type": "address" 35 | }, 36 | { 37 | "indexed": true, 38 | "internalType": "address", 39 | "name": "operator", 40 | "type": "address" 41 | }, 42 | { 43 | "indexed": false, 44 | "internalType": "bool", 45 | "name": "approved", 46 | "type": "bool" 47 | } 48 | ], 49 | "name": "ApprovalForAll", 50 | "type": "event" 51 | }, 52 | { 53 | "anonymous": false, 54 | "inputs": [ 55 | { 56 | "indexed": true, 57 | "internalType": "address", 58 | "name": "from", 59 | "type": "address" 60 | }, 61 | { 62 | "indexed": true, 63 | "internalType": "address", 64 | "name": "to", 65 | "type": "address" 66 | }, 67 | { 68 | "indexed": true, 69 | "internalType": "uint256", 70 | "name": "tokenId", 71 | "type": "uint256" 72 | } 73 | ], 74 | "name": "Transfer", 75 | "type": "event" 76 | }, 77 | { 78 | "constant": false, 79 | "inputs": [ 80 | { 81 | "internalType": "address", 82 | "name": "to", 83 | "type": "address" 84 | }, 85 | { 86 | "internalType": "uint256", 87 | "name": "tokenId", 88 | "type": "uint256" 89 | } 90 | ], 91 | "name": "approve", 92 | "outputs": [], 93 | "payable": false, 94 | "stateMutability": "nonpayable", 95 | "type": "function" 96 | }, 97 | { 98 | "constant": true, 99 | "inputs": [ 100 | { 101 | "internalType": "address", 102 | "name": "owner", 103 | "type": "address" 104 | } 105 | ], 106 | "name": "balanceOf", 107 | "outputs": [ 108 | { 109 | "internalType": "uint256", 110 | "name": "balance", 111 | "type": "uint256" 112 | } 113 | ], 114 | "payable": false, 115 | "stateMutability": "view", 116 | "type": "function" 117 | }, 118 | { 119 | "constant": true, 120 | "inputs": [ 121 | { 122 | "internalType": "uint256", 123 | "name": "tokenId", 124 | "type": "uint256" 125 | } 126 | ], 127 | "name": "getApproved", 128 | "outputs": [ 129 | { 130 | "internalType": "address", 131 | "name": "operator", 132 | "type": "address" 133 | } 134 | ], 135 | "payable": false, 136 | "stateMutability": "view", 137 | "type": "function" 138 | }, 139 | { 140 | "constant": true, 141 | "inputs": [ 142 | { 143 | "internalType": "address", 144 | "name": "owner", 145 | "type": "address" 146 | }, 147 | { 148 | "internalType": "address", 149 | "name": "operator", 150 | "type": "address" 151 | } 152 | ], 153 | "name": "isApprovedForAll", 154 | "outputs": [ 155 | { 156 | "internalType": "bool", 157 | "name": "", 158 | "type": "bool" 159 | } 160 | ], 161 | "payable": false, 162 | "stateMutability": "view", 163 | "type": "function" 164 | }, 165 | { 166 | "constant": true, 167 | "inputs": [], 168 | "name": "name", 169 | "outputs": [ 170 | { 171 | "internalType": "string", 172 | "name": "", 173 | "type": "string" 174 | } 175 | ], 176 | "payable": false, 177 | "stateMutability": "view", 178 | "type": "function" 179 | }, 180 | { 181 | "constant": true, 182 | "inputs": [ 183 | { 184 | "internalType": "uint256", 185 | "name": "tokenId", 186 | "type": "uint256" 187 | } 188 | ], 189 | "name": "ownerOf", 190 | "outputs": [ 191 | { 192 | "internalType": "address", 193 | "name": "owner", 194 | "type": "address" 195 | } 196 | ], 197 | "payable": false, 198 | "stateMutability": "view", 199 | "type": "function" 200 | }, 201 | { 202 | "constant": false, 203 | "inputs": [ 204 | { 205 | "internalType": "address", 206 | "name": "from", 207 | "type": "address" 208 | }, 209 | { 210 | "internalType": "address", 211 | "name": "to", 212 | "type": "address" 213 | }, 214 | { 215 | "internalType": "uint256", 216 | "name": "tokenId", 217 | "type": "uint256" 218 | } 219 | ], 220 | "name": "safeTransferFrom", 221 | "outputs": [], 222 | "payable": false, 223 | "stateMutability": "nonpayable", 224 | "type": "function" 225 | }, 226 | { 227 | "constant": false, 228 | "inputs": [ 229 | { 230 | "internalType": "address", 231 | "name": "from", 232 | "type": "address" 233 | }, 234 | { 235 | "internalType": "address", 236 | "name": "to", 237 | "type": "address" 238 | }, 239 | { 240 | "internalType": "uint256", 241 | "name": "tokenId", 242 | "type": "uint256" 243 | }, 244 | { 245 | "internalType": "bytes", 246 | "name": "data", 247 | "type": "bytes" 248 | } 249 | ], 250 | "name": "safeTransferFrom", 251 | "outputs": [], 252 | "payable": false, 253 | "stateMutability": "nonpayable", 254 | "type": "function" 255 | }, 256 | { 257 | "constant": false, 258 | "inputs": [ 259 | { 260 | "internalType": "address", 261 | "name": "operator", 262 | "type": "address" 263 | }, 264 | { 265 | "internalType": "bool", 266 | "name": "_approved", 267 | "type": "bool" 268 | } 269 | ], 270 | "name": "setApprovalForAll", 271 | "outputs": [], 272 | "payable": false, 273 | "stateMutability": "nonpayable", 274 | "type": "function" 275 | }, 276 | { 277 | "constant": true, 278 | "inputs": [ 279 | { 280 | "internalType": "bytes4", 281 | "name": "interfaceId", 282 | "type": "bytes4" 283 | } 284 | ], 285 | "name": "supportsInterface", 286 | "outputs": [ 287 | { 288 | "internalType": "bool", 289 | "name": "", 290 | "type": "bool" 291 | } 292 | ], 293 | "payable": false, 294 | "stateMutability": "view", 295 | "type": "function" 296 | }, 297 | { 298 | "constant": true, 299 | "inputs": [], 300 | "name": "symbol", 301 | "outputs": [ 302 | { 303 | "internalType": "string", 304 | "name": "", 305 | "type": "string" 306 | } 307 | ], 308 | "payable": false, 309 | "stateMutability": "view", 310 | "type": "function" 311 | }, 312 | { 313 | "constant": true, 314 | "inputs": [ 315 | { 316 | "internalType": "uint256", 317 | "name": "tokenId", 318 | "type": "uint256" 319 | } 320 | ], 321 | "name": "tokenURI", 322 | "outputs": [ 323 | { 324 | "internalType": "string", 325 | "name": "", 326 | "type": "string" 327 | } 328 | ], 329 | "payable": false, 330 | "stateMutability": "view", 331 | "type": "function" 332 | }, 333 | { 334 | "constant": false, 335 | "inputs": [ 336 | { 337 | "internalType": "address", 338 | "name": "from", 339 | "type": "address" 340 | }, 341 | { 342 | "internalType": "address", 343 | "name": "to", 344 | "type": "address" 345 | }, 346 | { 347 | "internalType": "uint256", 348 | "name": "tokenId", 349 | "type": "uint256" 350 | } 351 | ], 352 | "name": "transferFrom", 353 | "outputs": [], 354 | "payable": false, 355 | "stateMutability": "nonpayable", 356 | "type": "function" 357 | } 358 | ] -------------------------------------------------------------------------------- /src/assets/css/sakura-vader.css: -------------------------------------------------------------------------------- 1 | /* $color-text: #dedce5; */ 2 | 3 | /* Sakura.css v1.3.1 4 | * ================ 5 | * Minimal css theme. 6 | * Project: https://github.com/oxalorg/sakura/ 7 | */ 8 | 9 | /* Body */ 10 | 11 | html { 12 | font-size: 62.5%; 13 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; 14 | } 15 | 16 | body { 17 | font-size: 1.8rem; 18 | line-height: 1.618; 19 | max-width: 38em; 20 | margin: auto; 21 | color: #d9d8dc; 22 | background-color: #120c0e; 23 | padding: 13px; 24 | } 25 | 26 | @media (max-width: 684px) { 27 | body { 28 | font-size: 1.53rem; 29 | } 30 | } 31 | 32 | @media (max-width: 382px) { 33 | body { 34 | font-size: 1.35rem; 35 | } 36 | } 37 | 38 | h1, h2, h3, h4, h5, h6 { 39 | line-height: 1.1; 40 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; 41 | font-weight: 700; 42 | margin-top: 3rem; 43 | margin-bottom: 1.5rem; 44 | overflow-wrap: break-word; 45 | word-wrap: break-word; 46 | -ms-word-break: break-all; 47 | word-break: break-word; 48 | } 49 | 50 | h1 { 51 | font-size: 2.35em; 52 | } 53 | 54 | h2 { 55 | font-size: 2.00em; 56 | } 57 | 58 | h3 { 59 | font-size: 1.75em; 60 | } 61 | 62 | h4 { 63 | font-size: 1.5em; 64 | } 65 | 66 | h5 { 67 | font-size: 1.25em; 68 | } 69 | 70 | h6 { 71 | font-size: 1em; 72 | } 73 | 74 | p { 75 | margin-top: 0px; 76 | margin-bottom: 2.5rem; 77 | } 78 | 79 | small, sub, sup { 80 | font-size: 75%; 81 | } 82 | 83 | hr { 84 | border-color: #eb99a1; 85 | } 86 | 87 | a { 88 | text-decoration: none; 89 | color: #eb99a1; 90 | } 91 | 92 | a:hover { 93 | color: #DA4453; 94 | border-bottom: 2px solid #d9d8dc; 95 | } 96 | 97 | a:visited { 98 | color: #e26f7a; 99 | } 100 | 101 | ul { 102 | padding-left: 1.4em; 103 | margin-top: 0px; 104 | margin-bottom: 2.5rem; 105 | } 106 | 107 | li { 108 | margin-bottom: 0.4em; 109 | } 110 | 111 | blockquote { 112 | margin-left: 0px; 113 | margin-right: 0px; 114 | padding-left: 1em; 115 | padding-top: 0.8em; 116 | padding-bottom: 0.8em; 117 | padding-right: 0.8em; 118 | border-left: 5px solid #eb99a1; 119 | margin-bottom: 2.5rem; 120 | background-color: #40363a; 121 | } 122 | 123 | blockquote p { 124 | margin-bottom: 0; 125 | } 126 | 127 | img, video { 128 | height: auto; 129 | max-width: 100%; 130 | margin-top: 0px; 131 | margin-bottom: 2.5rem; 132 | } 133 | 134 | /* Pre and Code */ 135 | 136 | pre { 137 | background-color: #40363a; 138 | display: block; 139 | padding: 1em; 140 | overflow-x: auto; 141 | margin-top: 0px; 142 | margin-bottom: 2.5rem; 143 | } 144 | 145 | code { 146 | font-size: 0.9em; 147 | padding: 0 0.5em; 148 | background-color: #40363a; 149 | white-space: pre-wrap; 150 | } 151 | 152 | pre>code { 153 | padding: 0; 154 | background-color: transparent; 155 | white-space: pre; 156 | } 157 | 158 | /* Tables */ 159 | 160 | table { 161 | text-align: justify; 162 | width: 100%; 163 | border-collapse: collapse; 164 | } 165 | 166 | td, th { 167 | padding: 0.5em; 168 | border-bottom: 1px solid #40363a; 169 | } 170 | 171 | /* Buttons, forms and input */ 172 | 173 | input, textarea { 174 | border: 1px solid #d9d8dc; 175 | } 176 | 177 | input:focus, textarea:focus { 178 | border: 1px solid #eb99a1; 179 | } 180 | 181 | textarea { 182 | width: 100%; 183 | } 184 | 185 | .button, button, input[type="submit"], input[type="reset"], input[type="button"] { 186 | display: inline-block; 187 | padding: 5px 10px; 188 | text-align: center; 189 | text-decoration: none; 190 | white-space: nowrap; 191 | background-color: #eb99a1; 192 | color: #120c0e; 193 | border-radius: 1px; 194 | border: 1px solid #eb99a1; 195 | cursor: pointer; 196 | box-sizing: border-box; 197 | } 198 | 199 | .button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] { 200 | cursor: default; 201 | opacity: .5; 202 | } 203 | 204 | .button:focus:enabled, .button:hover:enabled, button:focus:enabled, button:hover:enabled, input[type="submit"]:focus:enabled, input[type="submit"]:hover:enabled, input[type="reset"]:focus:enabled, input[type="reset"]:hover:enabled, input[type="button"]:focus:enabled, input[type="button"]:hover:enabled { 205 | background-color: #DA4453; 206 | border-color: #DA4453; 207 | color: #120c0e; 208 | outline: 0; 209 | } 210 | 211 | textarea, select, input { 212 | color: #d9d8dc; 213 | padding: 6px 10px; 214 | /* The 6px vertically centers text on FF, ignored by Webkit */ 215 | margin-bottom: 10px; 216 | background-color: #40363a; 217 | border: 1px solid #40363a; 218 | border-radius: 4px; 219 | box-shadow: none; 220 | box-sizing: border-box; 221 | } 222 | 223 | textarea:focus, select:focus, input:focus { 224 | border: 1px solid #eb99a1; 225 | outline: 0; 226 | } 227 | 228 | input[type="checkbox"]:focus { 229 | outline: 1px dotted #eb99a1; 230 | } 231 | 232 | label, legend, fieldset { 233 | display: block; 234 | margin-bottom: .5rem; 235 | font-weight: 600; 236 | } -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/assets/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/assets/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/assets/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/assets/json/contracts.json: -------------------------------------------------------------------------------- 1 | { 2 | "Astrodrop": "0x594C62030eDbf4d09564bcE0efe2885b34B12e24", 3 | "AstrodropERC721": "0x4f96cccfd25b4b7a89062d52c3099e1a97793a99", 4 | "AstrodropFactory": "0x10da261f68feaa66d6455d1710b3818edd633444" 5 | } -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhalerDAO/astrodrop-frontend/1371e4cfb5c31d32614693f0fd594c9e0d6a33da/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Astrodrop 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/libs/ipfs-mini.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* eslint-disable */ 3 | (function webpackUniversalModuleDefinition(root, factory) { 4 | if(typeof exports === 'object' && typeof module === 'object') 5 | module.exports = factory(); 6 | else if(typeof define === 'function' && define.amd) 7 | define("IPFS", [], factory); 8 | else if(typeof exports === 'object') 9 | exports["IPFS"] = factory(); 10 | else 11 | root["IPFS"] = factory(); 12 | })(this, function() { 13 | return /******/ (function(modules) { // webpackBootstrap 14 | /******/ // The module cache 15 | /******/ var installedModules = {}; 16 | 17 | /******/ // The require function 18 | /******/ function __webpack_require__(moduleId) { 19 | 20 | /******/ // Check if module is in cache 21 | /******/ if(installedModules[moduleId]) 22 | /******/ return installedModules[moduleId].exports; 23 | 24 | /******/ // Create a new module (and put it into the cache) 25 | /******/ var module = installedModules[moduleId] = { 26 | /******/ i: moduleId, 27 | /******/ l: false, 28 | /******/ exports: {} 29 | /******/ }; 30 | 31 | /******/ // Execute the module function 32 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 33 | 34 | /******/ // Flag the module as loaded 35 | /******/ module.l = true; 36 | 37 | /******/ // Return the exports of the module 38 | /******/ return module.exports; 39 | /******/ } 40 | 41 | 42 | /******/ // expose the modules object (__webpack_modules__) 43 | /******/ __webpack_require__.m = modules; 44 | 45 | /******/ // expose the module cache 46 | /******/ __webpack_require__.c = installedModules; 47 | 48 | /******/ // identity function for calling harmory imports with the correct context 49 | /******/ __webpack_require__.i = function(value) { return value; }; 50 | 51 | /******/ // define getter function for harmory exports 52 | /******/ __webpack_require__.d = function(exports, name, getter) { 53 | /******/ Object.defineProperty(exports, name, { 54 | /******/ configurable: false, 55 | /******/ enumerable: true, 56 | /******/ get: getter 57 | /******/ }); 58 | /******/ }; 59 | 60 | /******/ // Object.prototype.hasOwnProperty.call 61 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 62 | 63 | /******/ // __webpack_public_path__ 64 | /******/ __webpack_require__.p = ""; 65 | 66 | /******/ // Load entry module and return exports 67 | /******/ return __webpack_require__(__webpack_require__.s = 1); 68 | /******/ }) 69 | /************************************************************************/ 70 | /******/ ([ 71 | /* 0 */ 72 | /***/ function(module, exports) { 73 | 74 | "use strict"; 75 | "use strict"; 76 | 77 | // var 78 | var XMLHttpRequest = window.XMLHttpRequest; // eslint-disable-line 79 | 80 | module.exports = XMLHttpRequest; 81 | 82 | /***/ }, 83 | /* 1 */ 84 | /***/ function(module, exports, __webpack_require__) { 85 | 86 | "use strict"; 87 | 'use strict'; 88 | 89 | var XMLHttpRequest = __webpack_require__(0); 90 | 91 | module.exports = IPFS; 92 | 93 | /** 94 | * The constructor object 95 | * @param {Object} `provider` the provider object 96 | * @return {Object} `ipfs` returns an IPFS instance 97 | * @throws if the `new` flag is not used 98 | */ 99 | function IPFS(provider) { 100 | if (!(this instanceof IPFS)) { 101 | throw new Error('[ipfs-mini] IPFS instance must be instantiated with "new" flag (e.g. var ipfs = new IPFS("http://localhost:8545");).'); 102 | } 103 | 104 | var self = this; 105 | self.setProvider(provider || {}); 106 | } 107 | 108 | /** 109 | * Sets the provider of the IPFS instance 110 | * @param {Object} `provider` the provider object 111 | * @throws if the provider object is not an object 112 | */ 113 | IPFS.prototype.setProvider = function setProvider(provider) { 114 | if (typeof provider !== 'object') { 115 | throw new Error('[ifpsjs] provider must be type Object, got \'' + typeof provider + '\'.'); 116 | } 117 | 118 | var self = this; 119 | var data = self.provider = Object.assign({ 120 | host: '127.0.0.1', 121 | pinning: true, 122 | port: '5001', 123 | protocol: 'http', 124 | base: '/api/v0' }, provider || {}); 125 | self.requestBase = String(data.protocol + '://' + data.host + data.base); 126 | }; 127 | 128 | 129 | 130 | /** 131 | * Sends an async data packet to an IPFS node 132 | * @param {Object} `opts` the options object 133 | * @param {Function} `cb` the provider callback 134 | * @callback returns an error if any, or the data from IPFS 135 | */ 136 | IPFS.prototype.sendAsync = function sendAsync(opts, cb) { 137 | var self = this; 138 | var request = new XMLHttpRequest(); // eslint-disable-line 139 | var options = opts || {}; 140 | var callback = cb || function emptyCallback() {}; 141 | 142 | request.onreadystatechange = function () { 143 | if (request.readyState === 4 && request.timeout !== 1) { 144 | if (request.status !== 200) { 145 | callback(new Error('[ipfs-mini] status ' + request.status + ': ' + request.responseText), null); 146 | } else { 147 | try { 148 | callback(null, options.jsonParse ? JSON.parse(request.responseText) : request.responseText); 149 | } catch (jsonError) { 150 | callback(new Error('[ipfs-mini] while parsing data: \'' + String(request.responseText) + '\', error: ' + String(jsonError) + ' with provider: \'' + self.requestBase + '\'', null)); 151 | } 152 | } 153 | } 154 | }; 155 | 156 | var pinningURI = self.provider.pinning && opts.uri === '/add' ? '?pin=true' : ''; 157 | 158 | if (options.payload) { 159 | request.open('POST', '' + self.requestBase + opts.uri + pinningURI); 160 | } else { 161 | request.open('GET', '' + self.requestBase + opts.uri + pinningURI); 162 | } 163 | 164 | if (options.accept) { 165 | request.setRequestHeader('accept', options.accept); 166 | } 167 | 168 | if (options.payload && options.boundary) { 169 | request.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + options.boundary); 170 | request.send(options.payload); 171 | } else { 172 | request.send(); 173 | } 174 | }; 175 | 176 | /** 177 | * creates a boundary that isn't part of the payload 178 | */ 179 | function createBoundary(data) { 180 | while (true) { 181 | var boundary = '----IPFSMini' + Math.random() * 100000 + '.' + Math.random() * 100000; 182 | if (data.indexOf(boundary) === -1) { 183 | return boundary; 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * Add an string or buffer to IPFS 190 | * @param {String|Buffer} `input` a single string or buffer 191 | * @param {Function} `callback` a callback, with (error, ipfsHash String) 192 | * @callback {String} `ipfsHash` returns an IPFS hash string 193 | */ 194 | IPFS.prototype.add = function addData(input, callback) { 195 | var data = typeof input === 'object' && input.isBuffer ? input.toString('binary') : input; 196 | var boundary = createBoundary(data); 197 | var payload = '--' + boundary + '\r\nContent-Disposition: form-data; name="path"\r\nContent-Type: application/octet-stream\r\n\r\n' + data + '\r\n--' + boundary + '--'; 198 | 199 | var addCallback = function addCallback(err, result) { 200 | return callback(err, !err ? result.Hash : null); 201 | }; 202 | this.sendAsync({ 203 | jsonParse: true, 204 | accept: 'application/json', 205 | uri: '/add', 206 | payload: payload, boundary: boundary 207 | }, addCallback); 208 | }; 209 | 210 | /** 211 | * Add an JSON object to IPFS 212 | * @param {Object} `jsonData` a single JSON object 213 | * @param {Function} `callback` a callback, with (error, ipfsHash String) 214 | * @callback {String} `ipfsHash` returns an IPFS hash string 215 | */ 216 | IPFS.prototype.addJSON = function addJson(jsonData, callback) { 217 | var self = this; 218 | self.add(JSON.stringify(jsonData), callback); 219 | }; 220 | 221 | /** 222 | * Get an object stat `/object/stat` for an IPFS hash 223 | * @param {String} `ipfsHash` a single IPFS hash String 224 | * @param {Function} `callback` a callback, with (error, stats Object) 225 | * @callback {Object} `stats` returns the stats object for that IPFS hash 226 | */ 227 | IPFS.prototype.stat = function cat(ipfsHash, callback) { 228 | var self = this; 229 | self.sendAsync({ jsonParse: true, uri: '/object/stat/' + ipfsHash }, callback); 230 | }; 231 | 232 | /** 233 | * Get the data from an IPFS hash 234 | * @param {String} `ipfsHash` a single IPFS hash String 235 | * @param {Function} `callback` a callback, with (error, stats Object) 236 | * @callback {String} `data` returns the output data 237 | */ 238 | IPFS.prototype.cat = function cat(ipfsHash, callback) { 239 | var self = this; 240 | self.sendAsync({ uri: ipfsHash, payload: false }, callback); 241 | }; 242 | 243 | /** 244 | * Get the data from an IPFS hash that is a JSON object 245 | * @param {String} `ipfsHash` a single IPFS hash String 246 | * @param {Function} `callback` a callback, with (error, json Object) 247 | * @callback {Object} `data` returns the output data JSON object 248 | */ 249 | IPFS.prototype.catJSON = function cat(ipfsHash, callback) { 250 | var self = this; 251 | self.cat(ipfsHash, function (jsonError, jsonResult) { 252 | // eslint-disable-line 253 | if (jsonError) { 254 | return callback(jsonError, null); 255 | } 256 | 257 | try { 258 | callback(null, JSON.parse(jsonResult)); 259 | } catch (jsonParseError) { 260 | callback(jsonParseError, null); 261 | } 262 | }); 263 | }; 264 | 265 | /***/ } 266 | /******/ ]) 267 | }); 268 | ; 269 | //# sourceMappingURL=ipfs-mini.js.map -------------------------------------------------------------------------------- /src/libs/ipfs-search-tree/interfaces.ts: -------------------------------------------------------------------------------- 1 | // Metadata interface 2 | export interface Metadata { 3 | name: string; 4 | description: string; 5 | logoURL: string; 6 | contractAddress: string; 7 | merkleRoot: string; 8 | tokenAddress: string; 9 | tokenTotal: string; // In hex 10 | tokenType: string; 11 | } 12 | 13 | // The root IPFS file 14 | export interface IPFSRoot { 15 | metadata: any; 16 | pivots: string[]; 17 | bins: string[]; 18 | keys: string[]; 19 | } 20 | -------------------------------------------------------------------------------- /src/libs/ipfs-search-tree/ipfs-helper.ts: -------------------------------------------------------------------------------- 1 | import IPFS from '../ipfs-mini'; 2 | import Pinata from '@pinata/sdk'; 3 | 4 | export class IPFSHelper { 5 | PINATA_KEY_PUBLIC = '2118d54c0ec9b0c87ac5'; 6 | PINATA_KEY_PRIVATE = 7 | '57f1b50a1cfaa88d64cafbde53e2814c450d812895fc08bb8e35fee366f3814e'; 8 | 9 | pinata: any; 10 | ipfs: any; 11 | 12 | constructor(ipfsEndpoint: string) { 13 | this.pinata = Pinata(this.PINATA_KEY_PUBLIC, this.PINATA_KEY_PRIVATE); 14 | this.ipfs = new IPFS({ 15 | host: ipfsEndpoint, 16 | protocol: 'https', 17 | base: '/ipfs/', 18 | }); 19 | } 20 | 21 | async getObjectFromIPFS(ipfsHash: string | null): Promise { 22 | if (ipfsHash === null) { 23 | return null; 24 | } 25 | return new Promise((resolve, reject) => { 26 | this.ipfs.catJSON(ipfsHash, (err, result) => { 27 | if (err != null) { 28 | reject(err); 29 | } else { 30 | resolve(result); 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | async uploadObjectToIPFS(value: any): Promise { 37 | const options = { 38 | pinataOptions: { 39 | cidVersion: 0, 40 | }, 41 | }; 42 | const response = await this.pinata.pinJSONToIPFS(value, options); 43 | return response.IpfsHash; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/libs/ipfs-search-tree/local-ipfs-search-tree.ts: -------------------------------------------------------------------------------- 1 | import { IPFSRoot, Metadata } from './interfaces'; 2 | import BigNumber from 'bignumber.js'; 3 | import { IPFSHelper } from './ipfs-helper'; 4 | 5 | // Used for creating & uploading a tree 6 | export class LocalIPFSSearchTree { 7 | ipfsHelper: IPFSHelper; 8 | keyValueMap: any; // maps string to object 9 | metadata: Metadata; 10 | updateProgress: any; // callback for updating progress bar 11 | binSize: number; 12 | uploadDelayMs: number; // the delay between bin IPFS uploads in ms 13 | 14 | constructor( 15 | ipfsEndpoint: string, 16 | data: any, 17 | metadata: Metadata, 18 | updateProgress: any, 19 | binSize: number = 500, 20 | uploadDelayMs: number = 100 21 | ) { 22 | this.ipfsHelper = new IPFSHelper(ipfsEndpoint); 23 | this.keyValueMap = data; 24 | this.metadata = metadata; 25 | this.updateProgress = updateProgress; 26 | this.binSize = binSize; 27 | this.uploadDelayMs = uploadDelayMs; 28 | } 29 | 30 | async uploadData(): Promise { 31 | // sort data keys 32 | const sortedKeys = Object.keys(this.keyValueMap).sort((a, b) => { 33 | const aNum = new BigNumber(a.substr(2).toLowerCase(), 16); 34 | const bNum = new BigNumber(b.substr(2).toLowerCase(), 16); 35 | if (aNum.eq(bNum)) { 36 | return 0; 37 | } 38 | return aNum.lt(bNum) ? -1 : 1; 39 | }); 40 | const N = sortedKeys.length; 41 | 42 | // divide data using pivots 43 | const pivots = []; 44 | const dataBins = []; 45 | const numBins = Math.ceil(N / this.binSize); 46 | for (let i = 1; i <= numBins; i++) { 47 | let pivotIdx = i * this.binSize - 1; 48 | if (pivotIdx >= N) { 49 | pivotIdx = N - 1; 50 | } 51 | const pivot = sortedKeys[pivotIdx]; 52 | pivots.push(pivot); 53 | 54 | const bin = {}; 55 | const binStartIdx = (i - 1) * this.binSize; 56 | for (let j = binStartIdx; j <= pivotIdx; j++) { 57 | const key = sortedKeys[j]; 58 | const value = this.keyValueMap[key]; 59 | bin[key] = value; 60 | } 61 | dataBins.push(bin); 62 | } 63 | 64 | // upload binned data 65 | function sleep(ms) { 66 | return new Promise((resolve) => setTimeout(resolve, ms)); 67 | } 68 | const binIPFSHashes = []; 69 | for (const value of dataBins) { 70 | const hash = await this.ipfsHelper.uploadObjectToIPFS(value); 71 | this.updateProgress(1 / numBins); 72 | binIPFSHashes.push(hash); 73 | await sleep(this.uploadDelayMs); 74 | } 75 | 76 | // construct root file 77 | const rootFile: IPFSRoot = { 78 | metadata: this.metadata, 79 | pivots, 80 | bins: binIPFSHashes, 81 | keys: sortedKeys, 82 | }; 83 | 84 | // upload root file 85 | const rootHash = await this.ipfsHelper.uploadObjectToIPFS(rootFile); 86 | return rootHash; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/libs/ipfs-search-tree/remote-ipfs-search-tree.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'bignumber.js'; 2 | import { IPFSRoot, Metadata } from './interfaces'; 3 | import { IPFSHelper } from './ipfs-helper'; 4 | 5 | // Used for searching a remote tree 6 | export class RemoteIPFSSearchTree { 7 | ipfsHelper: IPFSHelper; 8 | rootIPFSHash: string; 9 | rootFile: IPFSRoot; 10 | 11 | constructor(ipfsEndpoint: string, rootIPFSHash: string) { 12 | this.ipfsHelper = new IPFSHelper(ipfsEndpoint); 13 | this.rootIPFSHash = rootIPFSHash; 14 | } 15 | 16 | async init() { 17 | this.rootFile = await this.ipfsHelper.getObjectFromIPFS(this.rootIPFSHash); 18 | } 19 | 20 | async find(key: string): Promise { 21 | // linear search to find pivot 22 | for (let i = 0; i < this.rootFile.pivots.length; i++) { 23 | const pivot = this.rootFile.pivots[i]; 24 | const pivotNum = new BigNumber(pivot.substr(2).toLowerCase(), 16); 25 | const keyNum = new BigNumber(key.substr(2).toLowerCase(), 16); 26 | if (keyNum.lte(pivotNum)) { 27 | // found pivot, fetch bin 28 | const bin = await this.ipfsHelper.getObjectFromIPFS(this.rootFile.bins[i]); 29 | 30 | // find value in bin 31 | return bin[key]; 32 | } 33 | } 34 | return null; 35 | } 36 | 37 | get metadata(): Metadata { 38 | return this.rootFile.metadata; 39 | } 40 | } -------------------------------------------------------------------------------- /src/libs/merkle-tree/balance-tree.ts: -------------------------------------------------------------------------------- 1 | import MerkleTree from './merkle-tree' 2 | import { BigNumber, utils } from 'ethers' 3 | 4 | export default class BalanceTree { 5 | private readonly tree: MerkleTree 6 | constructor(balances: { account: string; amount: BigNumber }[]) { 7 | this.tree = new MerkleTree( 8 | balances.map(({ account, amount }, index) => { 9 | return BalanceTree.toNode(index, account, amount) 10 | }) 11 | ) 12 | } 13 | 14 | public static verifyProof( 15 | index: number | BigNumber, 16 | account: string, 17 | amount: BigNumber, 18 | proof: Buffer[], 19 | root: Buffer 20 | ): boolean { 21 | let pair = BalanceTree.toNode(index, account, amount) 22 | for (const item of proof) { 23 | pair = MerkleTree.combinedHash(pair, item) 24 | } 25 | 26 | return pair.equals(root) 27 | } 28 | 29 | // keccak256(abi.encode(index, account, amount)) 30 | public static toNode(index: number | BigNumber, account: string, amount: BigNumber): Buffer { 31 | return Buffer.from( 32 | utils.solidityKeccak256(['uint256', 'address', 'uint256'], [index, account, amount]).substr(2), 33 | 'hex' 34 | ) 35 | } 36 | 37 | public getHexRoot(): string { 38 | return this.tree.getHexRoot() 39 | } 40 | 41 | // returns the hex bytes32 values of the proof 42 | public getProof(index: number | BigNumber, account: string, amount: BigNumber): string[] { 43 | return this.tree.getHexProof(BalanceTree.toNode(index, account, amount)) 44 | } 45 | } -------------------------------------------------------------------------------- /src/libs/merkle-tree/merkle-tree.ts: -------------------------------------------------------------------------------- 1 | import { bufferToHex, keccak256 } from 'ethereumjs-util' 2 | 3 | export default class MerkleTree { 4 | private readonly elements: Buffer[] 5 | private readonly bufferElementPositionIndex: { [hexElement: string]: number } 6 | private readonly layers: Buffer[][] 7 | 8 | constructor(elements: Buffer[]) { 9 | this.elements = [...elements] 10 | // Sort elements 11 | this.elements.sort(Buffer.compare) 12 | // Deduplicate elements 13 | this.elements = MerkleTree.bufDedup(this.elements) 14 | 15 | this.bufferElementPositionIndex = this.elements.reduce<{ [hexElement: string]: number }>((memo, el, index) => { 16 | memo[bufferToHex(el)] = index 17 | return memo 18 | }, {}) 19 | 20 | // Create layers 21 | this.layers = this.getLayers(this.elements) 22 | } 23 | 24 | getLayers(elements: Buffer[]): Buffer[][] { 25 | if (elements.length === 0) { 26 | throw new Error('empty tree') 27 | } 28 | 29 | const layers = [] 30 | layers.push(elements) 31 | 32 | // Get next layer until we reach the root 33 | while (layers[layers.length - 1].length > 1) { 34 | layers.push(this.getNextLayer(layers[layers.length - 1])) 35 | } 36 | 37 | return layers 38 | } 39 | 40 | getNextLayer(elements: Buffer[]): Buffer[] { 41 | return elements.reduce((layer, el, idx, arr) => { 42 | if (idx % 2 === 0) { 43 | // Hash the current element with its pair element 44 | layer.push(MerkleTree.combinedHash(el, arr[idx + 1])) 45 | } 46 | 47 | return layer 48 | }, []) 49 | } 50 | 51 | static combinedHash(first: Buffer, second: Buffer): Buffer { 52 | if (!first) { 53 | return second 54 | } 55 | if (!second) { 56 | return first 57 | } 58 | 59 | return keccak256(MerkleTree.sortAndConcat(first, second)) 60 | } 61 | 62 | getRoot(): Buffer { 63 | return this.layers[this.layers.length - 1][0] 64 | } 65 | 66 | getHexRoot(): string { 67 | return bufferToHex(this.getRoot()) 68 | } 69 | 70 | getProof(el: Buffer) { 71 | let idx = this.bufferElementPositionIndex[bufferToHex(el)] 72 | 73 | if (typeof idx !== 'number') { 74 | throw new Error('Element does not exist in Merkle tree') 75 | } 76 | 77 | return this.layers.reduce((proof, layer) => { 78 | const pairElement = MerkleTree.getPairElement(idx, layer) 79 | 80 | if (pairElement) { 81 | proof.push(pairElement) 82 | } 83 | 84 | idx = Math.floor(idx / 2) 85 | 86 | return proof 87 | }, []) 88 | } 89 | 90 | getHexProof(el: Buffer): string[] { 91 | const proof = this.getProof(el) 92 | 93 | return MerkleTree.bufArrToHexArr(proof) 94 | } 95 | 96 | private static getPairElement(idx: number, layer: Buffer[]): Buffer | null { 97 | const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1 98 | 99 | if (pairIdx < layer.length) { 100 | return layer[pairIdx] 101 | } else { 102 | return null 103 | } 104 | } 105 | 106 | private static bufDedup(elements: Buffer[]): Buffer[] { 107 | return elements.filter((el, idx) => { 108 | return idx === 0 || !elements[idx - 1].equals(el) 109 | }) 110 | } 111 | 112 | private static bufArrToHexArr(arr: Buffer[]): string[] { 113 | if (arr.some((el) => !Buffer.isBuffer(el))) { 114 | throw new Error('Array is not an array of buffers') 115 | } 116 | 117 | return arr.map((el) => '0x' + el.toString('hex')) 118 | } 119 | 120 | private static sortAndConcat(...args: Buffer[]): Buffer { 121 | return Buffer.concat([...args].sort(Buffer.compare)) 122 | } 123 | } -------------------------------------------------------------------------------- /src/libs/merkle-tree/parse-balance-map.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, utils } from 'ethers' 2 | import BalanceTree from './balance-tree' 3 | 4 | const { isAddress, getAddress } = utils 5 | 6 | // This is the blob that gets distributed and pinned to IPFS. 7 | // It is completely sufficient for recreating the entire merkle tree. 8 | // Anyone can verify that all air drops are included in the tree, 9 | // and the tree has no additional distributions. 10 | interface MerkleDistributorInfo { 11 | merkleRoot: string 12 | tokenTotal: string 13 | claims: { 14 | [account: string]: { 15 | index: number 16 | amount: string 17 | proof: string[] 18 | flags?: { 19 | [flag: string]: boolean 20 | } 21 | } 22 | } 23 | } 24 | 25 | type OldFormat = { [account: string]: number | string } 26 | type NewFormat = { address: string; earnings: string; reasons: string } 27 | 28 | export function parseBalanceMap(balances: OldFormat | NewFormat[]): MerkleDistributorInfo { 29 | // if balances are in an old format, process them 30 | const balancesInNewFormat: NewFormat[] = Array.isArray(balances) 31 | ? balances 32 | : Object.keys(balances).map( 33 | (account): NewFormat => ({ 34 | address: account, 35 | earnings: `0x${balances[account].toString(16)}`, 36 | reasons: '', 37 | }) 38 | ) 39 | 40 | const dataByAddress = balancesInNewFormat.reduce<{ 41 | [address: string]: { amount: BigNumber; flags?: { [flag: string]: boolean } } 42 | }>((memo, { address: account, earnings, reasons }) => { 43 | if (!isAddress(account)) { 44 | throw new Error(`Found invalid address: ${account}`) 45 | } 46 | const parsed = getAddress(account) 47 | if (memo[parsed]) throw new Error(`Duplicate address: ${parsed}`) 48 | const parsedNum = BigNumber.from(earnings) 49 | if (parsedNum.lt(0)) throw new Error(`Invalid amount for account: ${account}`) 50 | 51 | const flags = { 52 | isSOCKS: reasons.includes('socks'), 53 | isLP: reasons.includes('lp'), 54 | isUser: reasons.includes('user'), 55 | } 56 | 57 | memo[parsed] = { amount: parsedNum, ...(reasons === '' ? {} : { flags }) } 58 | return memo 59 | }, {}) 60 | 61 | const sortedAddresses = Object.keys(dataByAddress).sort() 62 | 63 | // construct a tree 64 | const tree = new BalanceTree( 65 | sortedAddresses.map((address) => ({ account: address, amount: dataByAddress[address].amount })) 66 | ) 67 | 68 | // generate claims 69 | const claims = sortedAddresses.reduce<{ 70 | [address: string]: { amount: string; index: number; proof: string[]; flags?: { [flag: string]: boolean } } 71 | }>((memo, address, index) => { 72 | const { amount, flags } = dataByAddress[address] 73 | memo[address] = { 74 | index, 75 | amount: amount.toHexString(), 76 | proof: tree.getProof(index, address, amount), 77 | ...(flags ? { flags } : {}), 78 | } 79 | return memo 80 | }, {}) 81 | 82 | const tokenTotal: BigNumber = sortedAddresses.reduce( 83 | (memo, key) => memo.add(dataByAddress[key].amount), 84 | BigNumber.from(0) 85 | ) 86 | 87 | return { 88 | merkleRoot: tree.getHexRoot(), 89 | tokenTotal: tokenTotal.toHexString(), 90 | claims, 91 | } 92 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | .centered { 4 | display: block; 5 | margin-left: auto; 6 | margin-right: auto; 7 | } 8 | 9 | .glow-on-hover { 10 | width: 220px; 11 | height: 50px; 12 | border: none; 13 | outline: none; 14 | color: #111; 15 | background: #eb99a1; 16 | cursor: pointer; 17 | position: relative; 18 | z-index: 0; 19 | border-radius: 10px; 20 | } 21 | 22 | .glow-on-hover:before { 23 | content: ''; 24 | background: linear-gradient(45deg, #ff0000, #ff7300, #fffb00, #48ff00, #00ffd5, #002bff, #7a00ff, #ff00c8, #ff0000); 25 | position: absolute; 26 | top: -2px; 27 | left: -2px; 28 | background-size: 400%; 29 | z-index: -1; 30 | filter: blur(5px); 31 | width: calc(100% + 4px); 32 | height: calc(100% + 4px); 33 | animation: glowing 20s linear infinite; 34 | opacity: 0; 35 | transition: opacity .3s ease-in-out; 36 | border-radius: 10px; 37 | } 38 | 39 | .glow-on-hover:active { 40 | color: #eb99a1 41 | } 42 | 43 | .glow-on-hover:active:after { 44 | background: transparent; 45 | } 46 | 47 | .glow-on-hover:hover:before { 48 | opacity: 1; 49 | } 50 | 51 | .glow-on-hover:after { 52 | z-index: -1; 53 | content: ''; 54 | position: absolute; 55 | width: 100%; 56 | height: 100%; 57 | background: #eb99a1; 58 | left: 0; 59 | top: 0; 60 | border-radius: 10px; 61 | } 62 | 63 | @keyframes glowing { 64 | 0% { 65 | background-position: 0 0; 66 | } 67 | 50% { 68 | background-position: 400% 0; 69 | } 70 | 100% { 71 | background-position: 0 0; 72 | } 73 | } -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"], 10 | "angularCompilerOptions": { 11 | "enableIvy": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | --------------------------------------------------------------------------------