Compare commits
56 Commits
2397ebfe45
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ed03740852 | |||
| a3c1dfad2d | |||
| 9b410ace09 | |||
| 72b061749d | |||
| a42f228984 | |||
| 47b33a152d | |||
| ecffd976ac | |||
| f0f431b34e | |||
| b2457f0617 | |||
| 148b37dca3 | |||
| 1b83dad6b3 | |||
| a1bbb17b5d | |||
| 916c23e092 | |||
| 2c5116c5de | |||
| 441a514acc | |||
| 6f7388bb10 | |||
| fd7b9713ea | |||
| 7b5421e6e7 | |||
| 21f324aa12 | |||
| eeee9d9853 | |||
| 38fb66c694 | |||
| 14b8b50812 | |||
| f35b30e337 | |||
| 22f2e648a2 | |||
| 7973a1e8b7 | |||
| c50824adb6 | |||
| 257c476cc1 | |||
| 815109dec2 | |||
| 0673b01ac8 | |||
| 556554fbf3 | |||
| 94c7b6ddb4 | |||
| 715a43c097 | |||
| 0442b08623 | |||
| 0392e70b78 | |||
| a6bce1613b | |||
| 7626504480 | |||
| e86fbfa8e9 | |||
| 7d04d23a89 | |||
| dabf6dd60f | |||
| b1a864ce31 | |||
| 75a197947f | |||
| d446d5ab11 | |||
| 5a4a67e726 | |||
| 8750951de5 | |||
| cd84e7c3c9 | |||
| 5876efe29f | |||
| b9975cda10 | |||
| 826177c445 | |||
| 3baa74174d | |||
| b2ed48492b | |||
| ebf70dd10c | |||
| 488e9f45f1 | |||
| f5f53c6af4 | |||
| 2e49346533 | |||
| 0fdc45a031 | |||
| 956d79c3dc |
395
.dependency-cruiser.cjs
Normal file
@@ -0,0 +1,395 @@
|
||||
/** @type {import('dependency-cruiser').IConfiguration} */
|
||||
module.exports = {
|
||||
forbidden: [
|
||||
{
|
||||
name: 'no-circular',
|
||||
severity: 'warn',
|
||||
comment:
|
||||
'This dependency is part of a circular relationship. You might want to revise ' +
|
||||
'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ',
|
||||
from: {},
|
||||
to: {
|
||||
circular: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'no-orphans',
|
||||
comment:
|
||||
"This is an orphan module - it's likely not used (anymore?). Either use it or " +
|
||||
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
|
||||
"add an exception for it in your dependency-cruiser configuration. By default " +
|
||||
"this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " +
|
||||
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
|
||||
severity: 'warn',
|
||||
from: {
|
||||
orphan: true,
|
||||
pathNot: [
|
||||
'(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files
|
||||
'[.]d[.]ts$', // TypeScript declaration files
|
||||
'(^|/)tsconfig[.]json$', // TypeScript config
|
||||
'(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs
|
||||
]
|
||||
},
|
||||
to: {},
|
||||
},
|
||||
{
|
||||
name: 'no-deprecated-core',
|
||||
comment:
|
||||
'A module depends on a node core module that has been deprecated. Find an alternative - these are ' +
|
||||
"bound to exist - node doesn't deprecate lightly.",
|
||||
severity: 'warn',
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: [
|
||||
'core'
|
||||
],
|
||||
path: [
|
||||
'^v8/tools/codemap$',
|
||||
'^v8/tools/consarray$',
|
||||
'^v8/tools/csvparser$',
|
||||
'^v8/tools/logreader$',
|
||||
'^v8/tools/profile_view$',
|
||||
'^v8/tools/profile$',
|
||||
'^v8/tools/SourceMap$',
|
||||
'^v8/tools/splaytree$',
|
||||
'^v8/tools/tickprocessor-driver$',
|
||||
'^v8/tools/tickprocessor$',
|
||||
'^node-inspect/lib/_inspect$',
|
||||
'^node-inspect/lib/internal/inspect_client$',
|
||||
'^node-inspect/lib/internal/inspect_repl$',
|
||||
'^async_hooks$',
|
||||
'^punycode$',
|
||||
'^domain$',
|
||||
'^constants$',
|
||||
'^sys$',
|
||||
'^_linklist$',
|
||||
'^_stream_wrap$'
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'not-to-deprecated',
|
||||
comment:
|
||||
'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' +
|
||||
'version of that module, or find an alternative. Deprecated modules are a security risk.',
|
||||
severity: 'warn',
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: [
|
||||
'deprecated'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'no-non-package-json',
|
||||
severity: 'error',
|
||||
comment:
|
||||
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
|
||||
"That's problematic as the package either (1) won't be available on live (2 - worse) will be " +
|
||||
"available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " +
|
||||
"in your package.json.",
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: [
|
||||
'npm-no-pkg',
|
||||
'npm-unknown'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'not-to-unresolvable',
|
||||
comment:
|
||||
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
|
||||
'module: add it to your package.json. In all other cases you likely already know what to do.',
|
||||
severity: 'error',
|
||||
from: {},
|
||||
to: {
|
||||
couldNotResolve: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'no-duplicate-dep-types',
|
||||
comment:
|
||||
"Likely this module depends on an external ('npm') package that occurs more than once " +
|
||||
"in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
|
||||
"maintenance problems later on.",
|
||||
severity: 'warn',
|
||||
from: {},
|
||||
to: {
|
||||
moreThanOneDependencyType: true,
|
||||
// as it's pretty common to have a type import be a type only import
|
||||
// _and_ (e.g.) a devDependency - don't consider type-only dependency
|
||||
// types for this rule
|
||||
dependencyTypesNot: ["type-only"]
|
||||
}
|
||||
},
|
||||
|
||||
/* rules you might want to tweak for your specific situation: */
|
||||
|
||||
{
|
||||
name: 'not-to-spec',
|
||||
comment:
|
||||
'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' +
|
||||
"If there's something in a spec that's of use to other modules, it doesn't have that single " +
|
||||
'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.',
|
||||
severity: 'error',
|
||||
from: {},
|
||||
to: {
|
||||
path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'not-to-dev-dep',
|
||||
severity: 'error',
|
||||
comment:
|
||||
"This module depends on an npm package from the 'devDependencies' section of your " +
|
||||
'package.json. It looks like something that ships to production, though. To prevent problems ' +
|
||||
"with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
|
||||
'section of your package.json. If this module is development only - add it to the ' +
|
||||
'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration',
|
||||
from: {
|
||||
path: '^(src)',
|
||||
pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$'
|
||||
},
|
||||
to: {
|
||||
dependencyTypes: [
|
||||
'npm-dev',
|
||||
],
|
||||
// type only dependencies are not a problem as they don't end up in the
|
||||
// production code or are ignored by the runtime.
|
||||
dependencyTypesNot: [
|
||||
'type-only'
|
||||
],
|
||||
pathNot: [
|
||||
'node_modules/@types/'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'optional-deps-used',
|
||||
severity: 'info',
|
||||
comment:
|
||||
"This module depends on an npm package that is declared as an optional dependency " +
|
||||
"in your package.json. As this makes sense in limited situations only, it's flagged here. " +
|
||||
"If you're using an optional dependency here by design - add an exception to your" +
|
||||
"dependency-cruiser configuration.",
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: [
|
||||
'npm-optional'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'peer-deps-used',
|
||||
comment:
|
||||
"This module depends on an npm package that is declared as a peer dependency " +
|
||||
"in your package.json. This makes sense if your package is e.g. a plugin, but in " +
|
||||
"other cases - maybe not so much. If the use of a peer dependency is intentional " +
|
||||
"add an exception to your dependency-cruiser configuration.",
|
||||
severity: 'warn',
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: [
|
||||
'npm-peer'
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
options: {
|
||||
|
||||
/* Which modules not to follow further when encountered */
|
||||
doNotFollow: {
|
||||
/* path: an array of regular expressions in strings to match against */
|
||||
path: ['node_modules']
|
||||
},
|
||||
|
||||
/* Which modules to exclude */
|
||||
// exclude : {
|
||||
// /* path: an array of regular expressions in strings to match against */
|
||||
// path: '',
|
||||
// },
|
||||
|
||||
/* Which modules to exclusively include (array of regular expressions in strings)
|
||||
dependency-cruiser will skip everything not matching this pattern
|
||||
*/
|
||||
// includeOnly : [''],
|
||||
|
||||
/* List of module systems to cruise.
|
||||
When left out dependency-cruiser will fall back to the list of _all_
|
||||
module systems it knows of. It's the default because it's the safe option
|
||||
It might come at a performance penalty, though.
|
||||
moduleSystems: ['amd', 'cjs', 'es6', 'tsd']
|
||||
|
||||
As in practice only commonjs ('cjs') and ecmascript modules ('es6')
|
||||
are widely used, you can limit the moduleSystems to those.
|
||||
*/
|
||||
|
||||
// moduleSystems: ['cjs', 'es6'],
|
||||
|
||||
/*
|
||||
false: don't look at JSDoc imports (the default)
|
||||
true: dependency-cruiser will detect dependencies in JSDoc-style
|
||||
import statements. Implies "parser": "tsc", so the dependency-cruiser
|
||||
will use the typescript parser for JavaScript files.
|
||||
|
||||
For this to work the typescript compiler will need to be installed in the
|
||||
same spot as you're running dependency-cruiser from.
|
||||
*/
|
||||
// detectJSDocImports: true,
|
||||
|
||||
/* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/'
|
||||
to open it on your online repo or `vscode://file/${process.cwd()}/` to
|
||||
open it in visual studio code),
|
||||
*/
|
||||
// prefix: `vscode://file/${process.cwd()}/`,
|
||||
|
||||
/* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation
|
||||
true: also detect dependencies that only exist before typescript-to-javascript compilation
|
||||
"specify": for each dependency identify whether it only exists before compilation or also after
|
||||
*/
|
||||
// tsPreCompilationDeps: false,
|
||||
|
||||
/* list of extensions to scan that aren't javascript or compile-to-javascript.
|
||||
Empty by default. Only put extensions in here that you want to take into
|
||||
account that are _not_ parsable.
|
||||
*/
|
||||
// extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"],
|
||||
|
||||
/* if true combines the package.jsons found from the module up to the base
|
||||
folder the cruise is initiated from. Useful for how (some) mono-repos
|
||||
manage dependencies & dependency definitions.
|
||||
*/
|
||||
// combinedDependencies: false,
|
||||
|
||||
/* if true leave symlinks untouched, otherwise use the realpath */
|
||||
// preserveSymlinks: false,
|
||||
|
||||
/* TypeScript project file ('tsconfig.json') to use for
|
||||
(1) compilation and
|
||||
(2) resolution (e.g. with the paths property)
|
||||
|
||||
The (optional) fileName attribute specifies which file to take (relative to
|
||||
dependency-cruiser's current working directory). When not provided
|
||||
defaults to './tsconfig.json'.
|
||||
*/
|
||||
tsConfig: {
|
||||
fileName: 'jsconfig.json'
|
||||
},
|
||||
|
||||
/* Webpack configuration to use to get resolve options from.
|
||||
|
||||
The (optional) fileName attribute specifies which file to take (relative
|
||||
to dependency-cruiser's current working directory. When not provided defaults
|
||||
to './webpack.conf.js'.
|
||||
|
||||
The (optional) `env` and `arguments` attributes contain the parameters
|
||||
to be passed if your webpack config is a function and takes them (see
|
||||
webpack documentation for details)
|
||||
*/
|
||||
// webpackConfig: {
|
||||
// fileName: 'webpack.config.js',
|
||||
// env: {},
|
||||
// arguments: {}
|
||||
// },
|
||||
|
||||
/* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use
|
||||
for compilation
|
||||
*/
|
||||
// babelConfig: {
|
||||
// fileName: '.babelrc',
|
||||
// },
|
||||
|
||||
/* List of strings you have in use in addition to cjs/ es6 requires
|
||||
& imports to declare module dependencies. Use this e.g. if you've
|
||||
re-declared require, use a require-wrapper or use window.require as
|
||||
a hack.
|
||||
*/
|
||||
// exoticRequireStrings: [],
|
||||
|
||||
/* options to pass on to enhanced-resolve, the package dependency-cruiser
|
||||
uses to resolve module references to disk. The values below should be
|
||||
suitable for most situations
|
||||
|
||||
If you use webpack: you can also set these in webpack.conf.js. The set
|
||||
there will override the ones specified here.
|
||||
*/
|
||||
enhancedResolveOptions: {
|
||||
/* What to consider as an 'exports' field in package.jsons */
|
||||
exportsFields: ["exports"],
|
||||
/* List of conditions to check for in the exports field.
|
||||
Only works when the 'exportsFields' array is non-empty.
|
||||
*/
|
||||
conditionNames: ["import", "require", "node", "default", "types"],
|
||||
/* The extensions, by default are the same as the ones dependency-cruiser
|
||||
can access (run `npx depcruise --info` to see which ones that are in
|
||||
_your_ environment). If that list is larger than you need you can pass
|
||||
the extensions you actually use (e.g. [".js", ".jsx"]). This can speed
|
||||
up module resolution, which is the most expensive step.
|
||||
*/
|
||||
// extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
|
||||
/* What to consider a 'main' field in package.json */
|
||||
mainFields: ["module", "main", "types", "typings"],
|
||||
/* A list of alias fields in package.jsons
|
||||
|
||||
See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and
|
||||
the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields)
|
||||
documentation.
|
||||
|
||||
Defaults to an empty array (= don't use alias fields).
|
||||
*/
|
||||
// aliasFields: ["browser"],
|
||||
},
|
||||
|
||||
/* skipAnalysisNotInRules will make dependency-cruiser execute
|
||||
analysis strictly necessary for checking the rule set only.
|
||||
|
||||
See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules
|
||||
for details
|
||||
*/
|
||||
skipAnalysisNotInRules: true,
|
||||
|
||||
reporterOptions: {
|
||||
dot: {
|
||||
/* pattern of modules that can be consolidated in the detailed
|
||||
graphical dependency graph. The default pattern in this configuration
|
||||
collapses everything in node_modules to one folder deep so you see
|
||||
the external modules, but their innards.
|
||||
*/
|
||||
collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)',
|
||||
|
||||
/* Options to tweak the appearance of your graph.See
|
||||
https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions
|
||||
for details and some examples. If you don't specify a theme
|
||||
dependency-cruiser falls back to a built-in one.
|
||||
*/
|
||||
// theme: {
|
||||
// graph: {
|
||||
// /* splines: "ortho" gives straight lines, but is slow on big graphs
|
||||
// splines: "true" gives bezier curves (fast, not as nice as ortho)
|
||||
// */
|
||||
// splines: "true"
|
||||
// },
|
||||
// }
|
||||
},
|
||||
archi: {
|
||||
/* pattern of modules that can be consolidated in the high level
|
||||
graphical dependency graph. If you use the high level graphical
|
||||
dependency graph reporter (`archi`) you probably want to tweak
|
||||
this collapsePattern to your situation.
|
||||
*/
|
||||
collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)',
|
||||
|
||||
/* Options to tweak the appearance of your graph. If you don't specify a
|
||||
theme for 'archi' dependency-cruiser will use the one specified in the
|
||||
dot section above and otherwise use the default one.
|
||||
*/
|
||||
// theme: { },
|
||||
},
|
||||
"text": {
|
||||
"highlightFocused": true
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
// generated: dependency-cruiser@16.10.1 on 2025-04-24T20:50:32.854Z
|
||||
@@ -1 +1,2 @@
|
||||
VITE_WS_URL=wss://ws.dexorder.trade
|
||||
VITE_WS_URL=wss://ws.dexorder.com
|
||||
VITE_SHARE_URL=https://app.dexorder.com
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
VITE_WS_URL=ws://localhost:3001
|
||||
REQUIRE_AUTH=NOAUTH
|
||||
VITE_SHARE_URL=http://localhost:3001
|
||||
VITE_REQUIRE_APPROVAL=NO
|
||||
|
||||
2
bin/depcruise
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
npx depcruise src
|
||||
@@ -12,7 +12,16 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron&family=Saira+Semi+Condensed&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-L6F3Z6SBC7"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-L6F3Z6SBC7');
|
||||
</script>
|
||||
<div id="app"></div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.js"></script>
|
||||
<script src="/charting_library/charting_library.js"></script>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 197.74 264.44"><defs><style>.cls-1{fill:#35d721;}</style></defs><title>Asset 14</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_2-2" data-name="Layer 2"><polygon class="cls-1" points="99.15 0 0 129.6 53.91 129.6 53.78 248.87 145.01 129.6 197.74 129.6 99.15 0"/><polygon class="cls-1" points="95.07 264.44 145.02 199.16 144.98 264.44 95.07 264.44"/><polygon class="cls-1" points="53.79 264.44 145.04 145.15 145.04 156.62 62.55 264.44 53.79 264.44"/><polygon class="cls-1" points="74.39 264.44 145.02 172.11 145.02 183.57 83.16 264.44 74.39 264.44"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 623 B |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 749 KiB |
@@ -13,6 +13,7 @@
|
||||
"lint": "eslint . --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@isaacs/ttlcache": "^1.4.1",
|
||||
"@mdi/font": "6.9.96",
|
||||
"color": "^4.2.3",
|
||||
"core-js": "^3.29.0",
|
||||
@@ -24,6 +25,7 @@
|
||||
"pinia-plugin-persistedstate": "^4.1.3",
|
||||
"roboto-fontface": "*",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^3.2.0",
|
||||
"vue-router": "^4.0.0",
|
||||
"vue-scroll-picker": "^1.2.2",
|
||||
@@ -32,6 +34,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"dependency-cruiser": "^16.10.1",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"sass": "^1.60.0",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1487.72 264.73"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#35d721;}</style></defs><title>Asset 13</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_2-2" data-name="Layer 2"><path class="cls-1" d="M305.33,264.49c-37.72,0-56.58-20.37-56.58-60.73v-72c0-40.37,18.86-60.74,56.58-60.74h62.25V37.6L396.25.05V264.49ZM367.58,100.4H306.47C286.85,100.4,277,111,277,132.84V203c0,21.51,9.81,32.45,29.43,32.45h61.11Z"/><path class="cls-1" d="M472.26,264.59c-37.72,0-56.58-20.37-56.58-60.74v-72c0-40.36,18.86-60.73,56.58-60.73h34.33c38.1,0,57,20.37,57,60.73v47.91H443.22V203.1c0,21.5,9.8,32.44,29,32.44h91.29l-18.75,29Zm64.13-133.54c0-21.51-9.81-32.45-29.42-32.45H472.26c-19.24,0-29,10.94-29,32.45v23.76h93.17Z"/><path class="cls-1" d="M789,264.73c-50.17,0-62.62-16.22-62.62-65.64V136.85c0-51.68,15.09-65.64,62.62-65.64H816.2c49.8,0,62.62,16.6,62.62,65.64v62.24c0,43.76-12.45,65.64-51.68,65.64ZM850.53,132c0-20-6.79-31.31-24.52-31.31H778.86c-16.22,0-24.15,8.67-24.15,26.78v77c0,19.61,6.42,31.31,24.15,31.31H826c16.22,0,24.52-9.06,24.52-26.79Z"/><path class="cls-1" d="M927.06,263.88l-.4-131.68c0-21.88,9.81-32.44,29.42-32.44h61.12V70.34H955c-37.72,0-56.58,20.37-56.58,60.73l-.12,132.81Z"/><path class="cls-1" d="M1397.58,264.45l-.4-131.67c0-21.88,9.81-32.45,29.42-32.45h61.12V70.91h-62.25c-37.72,0-56.58,20.37-56.58,60.73l-.12,132.81Z"/><polygon class="cls-1" points="721.45 71.07 689.08 71.07 644.02 142.86 598.4 71.07 567.54 71.07 628.31 167.89 567.54 264.71 598.4 264.71 644.02 192.92 689.08 264.71 721.45 264.71 659.93 167.89 721.45 71.07"/><path class="cls-1" d="M1258.05,264.44c-37.72,0-56.58-20.37-56.58-60.73V131.65c0-40.36,18.86-60.73,56.58-60.73h34.33c38.1,0,57,20.37,57,60.73v47.91H1229V203c0,21.5,9.81,32.44,29,32.44h91.29v29.05Zm64.13-133.54c0-21.5-9.81-32.44-29.42-32.44h-34.71c-19.24,0-29,10.94-29,32.44v23.77h93.18Z"/><path class="cls-1" d="M1091.12,264.44c-37.72,0-56.58-20.37-56.58-60.73V131.65c0-40.36,18.86-60.73,56.58-60.73h62.25V37.55L1182,0V264.44Zm62.25-164.1h-61.12c-19.61,0-29.42,10.57-29.42,32.45V203c0,21.5,9.81,32.44,29.42,32.44h61.12Z"/><polygon class="cls-2" points="99.15 0.05 0 129.65 53.91 129.65 53.78 248.92 145.01 129.65 197.74 129.65 99.15 0.05"/><polygon class="cls-2" points="95.07 264.49 145.02 199.22 144.98 264.49 95.07 264.49"/><polygon class="cls-2" points="53.79 264.49 145.04 145.2 145.04 156.67 62.55 264.49 53.79 264.49"/><polygon class="cls-2" points="74.39 264.49 145.02 172.16 145.02 183.62 83.16 264.49 74.39 264.49"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1487.72 264.73"><defs><style>.cls-1{fill:#0f0f0f;}.cls-2{fill:#35d721;}</style></defs><title>Asset 12</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_2-2" data-name="Layer 2"><path class="cls-1" d="M305.33,264.49c-37.72,0-56.58-20.37-56.58-60.73v-72c0-40.37,18.86-60.74,56.58-60.74h62.25V37.6L396.25.05V264.49ZM367.58,100.4H306.47C286.85,100.4,277,111,277,132.84V203c0,21.51,9.81,32.45,29.43,32.45h61.11Z"/><path class="cls-1" d="M472.26,264.59c-37.72,0-56.58-20.37-56.58-60.74v-72c0-40.36,18.86-60.73,56.58-60.73h34.33c38.1,0,57,20.37,57,60.73v47.91H443.22V203.1c0,21.5,9.8,32.44,29,32.44h91.29l-18.75,29Zm64.13-133.54c0-21.51-9.81-32.45-29.42-32.45H472.26c-19.24,0-29,10.94-29,32.45v23.76h93.17Z"/><path class="cls-1" d="M789,264.73c-50.17,0-62.62-16.22-62.62-65.64V136.85c0-51.68,15.09-65.64,62.62-65.64H816.2c49.8,0,62.62,16.6,62.62,65.64v62.24c0,43.76-12.45,65.64-51.68,65.64ZM850.53,132c0-20-6.79-31.31-24.52-31.31H778.86c-16.22,0-24.15,8.67-24.15,26.78v77c0,19.61,6.42,31.31,24.15,31.31H826c16.22,0,24.52-9.06,24.52-26.79Z"/><path class="cls-1" d="M927.06,263.88l-.4-131.68c0-21.88,9.81-32.44,29.42-32.44h61.12V70.34H955c-37.72,0-56.58,20.37-56.58,60.73l-.12,132.81Z"/><path class="cls-1" d="M1397.58,264.45l-.4-131.67c0-21.88,9.81-32.45,29.42-32.45h61.12V70.91h-62.25c-37.72,0-56.58,20.37-56.58,60.73l-.12,132.81Z"/><polygon class="cls-1" points="721.45 71.07 689.08 71.07 644.02 142.86 598.4 71.07 567.54 71.07 628.31 167.89 567.54 264.71 598.4 264.71 644.02 192.92 689.08 264.71 721.45 264.71 659.93 167.89 721.45 71.07"/><path class="cls-1" d="M1258.05,264.44c-37.72,0-56.58-20.37-56.58-60.73V131.65c0-40.36,18.86-60.73,56.58-60.73h34.33c38.1,0,57,20.37,57,60.73v47.91H1229V203c0,21.5,9.81,32.44,29,32.44h91.29v29.05Zm64.13-133.54c0-21.5-9.81-32.44-29.42-32.44h-34.71c-19.24,0-29,10.94-29,32.44v23.77h93.18Z"/><path class="cls-1" d="M1091.12,264.44c-37.72,0-56.58-20.37-56.58-60.73V131.65c0-40.36,18.86-60.73,56.58-60.73h62.25V37.55L1182,0V264.44Zm62.25-164.1h-61.12c-19.61,0-29.42,10.57-29.42,32.45V203c0,21.5,9.81,32.44,29.42,32.44h61.12Z"/><polygon class="cls-2" points="99.15 0.05 0 129.65 53.91 129.65 53.78 248.92 145.01 129.65 197.74 129.65 99.15 0.05"/><polygon class="cls-2" points="95.07 264.49 145.02 199.22 144.98 264.49 95.07 264.49"/><polygon class="cls-2" points="53.79 264.49 145.04 145.2 145.04 156.67 62.55 264.49 53.79 264.49"/><polygon class="cls-2" points="74.39 264.49 145.02 172.16 145.02 183.62 83.16 264.49 74.39 264.49"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
11
public/uniswap-logo.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="641" height="640" viewBox="0 0 641 640" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M224.534 123.226C218.692 122.32 218.445 122.213 221.195 121.791C226.464 120.98 238.905 122.085 247.479 124.123C267.494 128.881 285.707 141.069 305.148 162.714L310.313 168.465L317.701 167.277C348.828 162.275 380.493 166.25 406.978 178.485C414.264 181.851 425.752 188.552 427.187 190.274C427.645 190.822 428.485 194.355 429.053 198.124C431.02 211.164 430.036 221.16 426.047 228.625C423.877 232.688 423.756 233.975 425.215 237.452C426.38 240.227 429.627 242.28 432.843 242.276C439.425 242.267 446.509 231.627 449.791 216.823L451.095 210.943L453.678 213.868C467.846 229.92 478.974 251.811 480.885 267.393L481.383 271.455L479.002 267.762C474.903 261.407 470.785 257.08 465.512 253.591C456.006 247.301 445.955 245.161 419.337 243.758C395.296 242.491 381.69 240.438 368.198 236.038C345.244 228.554 333.672 218.587 306.405 182.812C294.294 166.923 286.808 158.131 279.362 151.051C262.442 134.964 245.816 126.527 224.534 123.226Z" fill="#FF007A"/>
|
||||
<path d="M432.61 158.704C433.215 148.057 434.659 141.033 437.562 134.62C438.711 132.081 439.788 130.003 439.954 130.003C440.12 130.003 439.621 131.877 438.844 134.167C436.733 140.392 436.387 148.905 437.84 158.811C439.686 171.379 440.735 173.192 454.019 186.769C460.25 193.137 467.497 201.168 470.124 204.616L474.901 210.886L470.124 206.405C464.282 200.926 450.847 190.24 447.879 188.712C445.89 187.688 445.594 187.705 444.366 188.927C443.235 190.053 442.997 191.744 442.84 199.741C442.596 212.204 440.897 220.204 436.797 228.203C434.58 232.529 434.23 231.606 436.237 226.723C437.735 223.077 437.887 221.474 437.876 209.408C437.853 185.167 434.975 179.339 418.097 169.355C413.821 166.826 406.776 163.178 402.442 161.249C398.107 159.32 394.664 157.639 394.789 157.514C395.267 157.038 411.727 161.842 418.352 164.39C428.206 168.181 429.833 168.672 431.03 168.215C431.832 167.909 432.22 165.572 432.61 158.704Z" fill="#FF007A"/>
|
||||
<path d="M235.883 200.175C224.022 183.846 216.684 158.809 218.272 140.093L218.764 134.301L221.463 134.794C226.534 135.719 235.275 138.973 239.369 141.459C250.602 148.281 255.465 157.263 260.413 180.328C261.862 187.083 263.763 194.728 264.638 197.317C266.047 201.483 271.369 211.214 275.696 217.534C278.813 222.085 276.743 224.242 269.853 223.62C259.331 222.67 245.078 212.834 235.883 200.175Z" fill="#FF007A"/>
|
||||
<path d="M418.223 321.707C362.793 299.389 343.271 280.017 343.271 247.331C343.271 242.521 343.437 238.585 343.638 238.585C343.84 238.585 345.985 240.173 348.404 242.113C359.644 251.128 372.231 254.979 407.076 260.062C427.58 263.054 439.119 265.47 449.763 269C483.595 280.22 504.527 302.99 509.518 334.004C510.969 343.016 510.118 359.915 507.766 368.822C505.91 375.857 500.245 388.537 498.742 389.023C498.325 389.158 497.917 387.562 497.81 385.389C497.24 373.744 491.355 362.406 481.472 353.913C470.235 344.257 455.137 336.569 418.223 321.707Z" fill="#FF007A"/>
|
||||
<path d="M379.31 330.978C378.615 326.846 377.411 321.568 376.633 319.25L375.219 315.036L377.846 317.985C381.481 322.065 384.354 327.287 386.789 334.241C388.647 339.549 388.856 341.127 388.842 349.753C388.828 358.221 388.596 359.996 386.88 364.773C384.174 372.307 380.816 377.649 375.181 383.383C365.056 393.688 352.038 399.393 333.253 401.76C329.987 402.171 320.47 402.864 312.103 403.299C291.016 404.395 277.138 406.661 264.668 411.04C262.875 411.67 261.274 412.052 261.112 411.89C260.607 411.388 269.098 406.326 276.111 402.948C285.999 398.185 295.842 395.586 317.897 391.913C328.792 390.098 340.043 387.897 342.9 387.021C369.88 378.749 383.748 357.402 379.31 330.978Z" fill="#FF007A"/>
|
||||
<path d="M404.719 376.105C397.355 360.273 395.664 344.988 399.698 330.732C400.13 329.209 400.824 327.962 401.242 327.962C401.659 327.962 403.397 328.902 405.103 330.05C408.497 332.335 415.303 336.182 433.437 346.069C456.065 358.406 468.966 367.959 477.74 378.873C485.423 388.432 490.178 399.318 492.467 412.593C493.762 420.113 493.003 438.206 491.074 445.778C484.99 469.653 470.85 488.406 450.682 499.349C447.727 500.952 445.075 502.269 444.788 502.275C444.501 502.28 445.577 499.543 447.18 496.191C453.965 482.009 454.737 468.214 449.608 452.859C446.467 443.457 440.064 431.985 427.135 412.596C412.103 390.054 408.417 384.054 404.719 376.105Z" fill="#FF007A"/>
|
||||
<path d="M196.519 461.525C217.089 444.157 242.682 431.819 265.996 428.032C276.043 426.399 292.78 427.047 302.084 429.428C316.998 433.245 330.338 441.793 337.276 451.978C344.057 461.932 346.966 470.606 349.995 489.906C351.189 497.519 352.489 505.164 352.882 506.895C355.156 516.897 359.583 524.892 365.067 528.907C373.779 535.283 388.78 535.68 403.536 529.924C406.041 528.947 408.215 528.271 408.368 528.424C408.903 528.955 401.473 533.93 396.23 536.548C389.177 540.071 383.568 541.434 376.115 541.434C362.6 541.434 351.379 534.558 342.016 520.539C340.174 517.78 336.032 509.516 332.813 502.176C322.928 479.628 318.046 472.759 306.568 465.242C296.579 458.701 283.697 457.53 274.006 462.282C261.276 468.523 257.724 484.791 266.842 495.101C270.465 499.198 277.223 502.732 282.749 503.419C293.086 504.705 301.97 496.841 301.97 486.404C301.97 479.627 299.365 475.76 292.808 472.801C283.852 468.76 274.226 473.483 274.272 481.897C274.292 485.484 275.854 487.737 279.45 489.364C281.757 490.408 281.811 490.491 279.929 490.1C271.712 488.396 269.787 478.49 276.394 471.913C284.326 464.018 300.729 467.502 306.362 478.279C308.728 482.805 309.003 491.82 306.94 497.264C302.322 509.448 288.859 515.855 275.201 512.368C265.903 509.994 262.117 507.424 250.906 495.876C231.425 475.809 223.862 471.92 195.777 467.536L190.395 466.696L196.519 461.525Z" fill="#FF007A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6202 12.0031C114.678 90.9638 214.977 213.901 219.957 220.784C224.068 226.467 222.521 231.576 215.478 235.58C211.561 237.807 203.508 240.063 199.476 240.063C194.916 240.063 189.779 237.867 186.038 234.318C183.393 231.81 172.721 215.874 148.084 177.646C129.233 148.396 113.457 124.131 113.027 123.725C112.032 122.785 112.049 122.817 146.162 183.854C167.582 222.181 174.813 235.731 174.813 237.543C174.813 241.229 173.808 243.166 169.261 248.238C161.681 256.694 158.293 266.195 155.847 285.859C153.104 307.902 145.394 323.473 124.026 350.122C111.518 365.722 109.471 368.581 106.315 374.869C102.339 382.786 101.246 387.221 100.803 397.219C100.335 407.79 101.247 414.619 104.477 424.726C107.304 433.575 110.255 439.417 117.8 451.104C124.311 461.188 128.061 468.683 128.061 471.614C128.061 473.947 128.506 473.95 138.596 471.672C162.741 466.219 182.348 456.629 193.375 444.877C200.199 437.603 201.801 433.586 201.853 423.618C201.887 417.098 201.658 415.733 199.896 411.982C197.027 405.877 191.804 400.801 180.292 392.932C165.209 382.621 158.767 374.32 156.987 362.904C155.527 353.537 157.221 346.928 165.565 329.44C174.202 311.338 176.342 303.624 177.79 285.378C178.725 273.589 180.02 268.94 183.407 265.209C186.939 261.317 190.119 260 198.861 258.805C213.113 256.858 222.188 253.171 229.648 246.297C236.119 240.334 238.827 234.588 239.243 225.938L239.558 219.382L235.942 215.166C222.846 199.896 40.85 0 40.044 0C39.8719 0 44.1813 5.40178 49.6202 12.0031ZM135.412 409.18C138.373 403.937 136.8 397.195 131.847 393.902C127.167 390.79 119.897 392.256 119.897 396.311C119.897 397.548 120.582 398.449 122.124 399.243C124.72 400.579 124.909 402.081 122.866 405.152C120.797 408.262 120.964 410.996 123.337 412.854C127.162 415.849 132.576 414.202 135.412 409.18Z" fill="#FF007A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M248.552 262.244C241.862 264.299 235.358 271.39 233.344 278.826C232.116 283.362 232.813 291.319 234.653 293.776C237.625 297.745 240.499 298.791 248.282 298.736C263.518 298.63 276.764 292.095 278.304 283.925C279.567 277.229 273.749 267.948 265.736 263.874C261.601 261.772 252.807 260.938 248.552 262.244ZM266.364 276.172C268.714 272.834 267.686 269.225 263.69 266.785C256.08 262.138 244.571 265.983 244.571 273.173C244.571 276.752 250.572 280.656 256.074 280.656C259.735 280.656 264.746 278.473 266.364 276.172Z" fill="#FF007A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
@@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<router-view/>
|
||||
<support-chat/>
|
||||
<welcome-dialog v-model="prefs.newbie"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SupportChat from "@/components/SupportChat.vue";
|
||||
import {detectChain} from "@/blockchain/wallet.js";
|
||||
detectChain()
|
||||
import WelcomeDialog from "@/components/WelcomeDialog.vue";
|
||||
import {usePrefStore} from "@/store/store.js";
|
||||
|
||||
const prefs = usePrefStore()
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {provider as walletProvider} from "@/blockchain/provider.js";
|
||||
import {ethers} from "ethers";
|
||||
import {AbiURLCache} from "../common.js";
|
||||
import {provider as walletProvider} from "@/blockchain/wallet.js";
|
||||
|
||||
export const abiCache = new AbiURLCache('/contract/out/')
|
||||
|
||||
|
||||
@@ -10,13 +10,31 @@ export function subOHLC( chainId, pool, period ) {
|
||||
// console.log('subOHLC', chainId, pool, period, ckey, ohlcSubCounts[ckey])
|
||||
if (!(ckey in ohlcSubCounts) || ohlcSubCounts[ckey] === 0) {
|
||||
ohlcSubCounts[ckey] = 1
|
||||
console.log('subscribing OHLCs', chainId, key)
|
||||
// console.log('subscribing OHLCs', chainId, key)
|
||||
socket.emit('subOHLCs', chainId, [key])
|
||||
} else
|
||||
ohlcSubCounts[ckey]++
|
||||
}
|
||||
|
||||
|
||||
export function refreshOHLCSubs() {
|
||||
const keys = []
|
||||
let chainId = null
|
||||
for (const key of Object.keys(ohlcSubCounts)) {
|
||||
const [curChainId, pool, period] = key.split('|')
|
||||
if (chainId === null)
|
||||
chainId = curChainId
|
||||
else if (chainId !== curChainId) {
|
||||
console.error('refreshOHLCSubs: mixed chainIds')
|
||||
continue
|
||||
}
|
||||
keys.push(`${pool}|${period}`)
|
||||
}
|
||||
// console.log('refreshing OHLC subs', keys)
|
||||
socket.emit('subOHLCs', chainId, keys)
|
||||
}
|
||||
|
||||
|
||||
export function unsubOHLC( chainId, pool, period ) {
|
||||
const key = `${pool}|${period}`
|
||||
const ckey = `${chainId}|${key}`
|
||||
@@ -27,7 +45,7 @@ export function unsubOHLC( chainId, pool, period ) {
|
||||
} else {
|
||||
ohlcSubCounts[ckey]--
|
||||
if (ohlcSubCounts[ckey] === 0) {
|
||||
console.log('unsubscribing OHLCs', chainId, key)
|
||||
// console.log('unsubscribing OHLCs', chainId, key)
|
||||
// noinspection JSCheckFunctionSignatures
|
||||
socket.emit('unsubOHLCs', chainId, [key])
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import {uint32max, uint64max} from "@/misc.js";
|
||||
import {encodeIEE754} from "@/common.js";
|
||||
|
||||
|
||||
export const MAX_FRACTION = 65535;
|
||||
export const NO_CONDITIONAL_ORDER = uint64max;
|
||||
export const NO_OCO = uint64max;
|
||||
export const DISTANT_PAST = 0
|
||||
export const DISTANT_FUTURE = uint32max
|
||||
|
||||
export const MIN_EXECUTION_TIME = 60 // give at least one full minute for each tranche to trigger
|
||||
export const DEFAULT_SLIPPAGE = 0.0030;
|
||||
export const MIN_SLIPPAGE = 0.0001;
|
||||
|
||||
// struct SwapOrder {
|
||||
// address tokenIn;
|
||||
// address tokenOut;
|
||||
@@ -31,7 +36,7 @@ export function newOrder(tokenIn, tokenOut, exchange, fee, amount, amountIsInput
|
||||
if (!tranches)
|
||||
tranches = [newTranche({marketOrder: true})] // todo this is just a swap: issue warning?
|
||||
if( minFillAmount === null )
|
||||
minFillAmount = amount / 100n // default to min trade size of 1%
|
||||
minFillAmount = amount / 1000n // default to min trade size of 0.1%
|
||||
return {
|
||||
tokenIn, tokenOut, route:{exchange, fee},
|
||||
amount, minFillAmount, amountIsInput,
|
||||
@@ -81,11 +86,13 @@ export function newTranche({
|
||||
rateLimitFraction = 0,
|
||||
rateLimitPeriod = 0,
|
||||
} = {}) {
|
||||
if( minIntercept === 0 && minSlope === 0 && maxIntercept === 0 && maxSlope === 0 )
|
||||
marketOrder = true
|
||||
if( marketOrder ) {
|
||||
if (minIntercept !== 0 || minSlope !== 0 || maxIntercept !== 0 || maxSlope !== 0)
|
||||
console.warn('Ignoring line information in a market order')
|
||||
throw Error('Cannot set line information on a market order')
|
||||
if (slippage === 0)
|
||||
slippage = DEFAULT_SLIPPAGE
|
||||
else if (slippage < MIN_SLIPPAGE)
|
||||
slippage = MIN_SLIPPAGE
|
||||
minIntercept = encodeIEE754(slippage) // this is the slippage field for market orders
|
||||
minSlope = 0
|
||||
maxIntercept = 0
|
||||
@@ -137,7 +144,7 @@ export function parseElaboratedOrderStatus(chainId, status) {
|
||||
|
||||
|
||||
export function parseOrderStatus(chainId, status) {
|
||||
console.log('parseOrderStatus', status)
|
||||
// console.log('parseOrderStatus', status)
|
||||
let [
|
||||
order,
|
||||
fillFeeHalfBps,
|
||||
@@ -158,7 +165,7 @@ export function parseOrderStatus(chainId, status) {
|
||||
chainId, order, fillFeeHalfBps, state, startTime, startPrice, ocoGroup,
|
||||
filledIn, filledOut, filled, trancheStatus,
|
||||
};
|
||||
console.log('SwapOrderStatus', result)
|
||||
// console.log('SwapOrderStatus', result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -250,4 +257,3 @@ export function parseFeeSchedule(sched) {
|
||||
fillFee: fillFeeHalfBps/1_000_000 // fillFee is a multiplier on the filled volume. 0.0001 = 0.1% of the output token taken as a fee
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {socket} from "@/socket.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {Exchange} from "@/blockchain/orderlib.js";
|
||||
import {uniswapV3PoolAddress} from "@/blockchain/uniswap.js";
|
||||
import {FixedNumber} from "ethers";
|
||||
import {provider} from "@/blockchain/wallet.js";
|
||||
import {newContract} from "@/blockchain/contract.js";
|
||||
import {provider} from "@/blockchain/provider.js";
|
||||
import {socket} from "@/socket.js";
|
||||
|
||||
const subscriptionCounts = {} // key is route and value is a subscription counter
|
||||
export const WIDE_PRICE_FORMAT = {decimals:38, width:512, signed:false}; // 38 decimals is 127 bits
|
||||
|
||||
3
src/blockchain/provider.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export let provider = null
|
||||
|
||||
export function setProvider(p) {provider = p}
|
||||
@@ -2,7 +2,8 @@ import {Exchange} from "@/blockchain/orderlib.js";
|
||||
import {useOrderStore, useStore} from "@/store/store.js";
|
||||
import {queryHelperContract} from "@/blockchain/contract.js";
|
||||
import {SingletonCoroutine} from "@/misc.js";
|
||||
import {provider} from "@/blockchain/wallet.js";
|
||||
|
||||
import {provider} from "@/blockchain/provider.js";
|
||||
|
||||
|
||||
export async function findRoute(helper, chainId, tokenA, tokenB) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {socket} from "@/socket.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {metadataMap} from "@/version.js";
|
||||
import {provider} from "@/blockchain/wallet.js";
|
||||
import {newContract} from "@/blockchain/contract.js";
|
||||
import {provider} from "@/blockchain/provider.js";
|
||||
import {socket} from "@/socket.js";
|
||||
|
||||
|
||||
// synchronous version may return null but will trigger a lookup
|
||||
@@ -31,7 +31,16 @@ export async function getToken(chainId, addr) {
|
||||
return found
|
||||
if (!(addr in s.tokens))
|
||||
await addExtraToken(chainId, addr)
|
||||
return s.tokens[addr]
|
||||
let result = s.tokens[addr]
|
||||
if (!result) {
|
||||
result = {
|
||||
n: addr,
|
||||
a: addr,
|
||||
s: addr,
|
||||
d: 0,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import {nav, uuid} from "@/misc.js";
|
||||
import {newContract, vaultContract} from "@/blockchain/contract.js";
|
||||
import {ensureVault, provider, switchChain, useWalletStore} from "@/blockchain/wallet.js";
|
||||
import {provider} from "@/blockchain/provider.js";
|
||||
import {TransactionState, TransactionType} from "@/blockchain/transactionDecl.js";
|
||||
import {sleep, uuid} from "@/misc.js";
|
||||
import {vaultContract} from "@/blockchain/contract.js";
|
||||
import {switchChain, useWalletStore} from "@/blockchain/wallet.js";
|
||||
import {toRaw} from "vue";
|
||||
import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import {timestamp} from "@/common.js";
|
||||
import {placementFee} from "@/fees.js";
|
||||
import {router} from "@/router/router.js";
|
||||
|
||||
export const TransactionState = {
|
||||
Created: 0, // user requested a transaction
|
||||
Proposed: 1, // tx is sent to the wallet
|
||||
Signed: 2, // tx is awaiting blockchain mining
|
||||
Rejected: 3, // user refused to sign the tx
|
||||
Error: 3, // unknown error sending the tx to the wallet
|
||||
Mined: 4, // transaction has been confirmed on-chain
|
||||
}
|
||||
|
||||
export const TransactionType = {
|
||||
PlaceOrder: 1,
|
||||
CancelOrder: 2,
|
||||
CancelAll: 3,
|
||||
Wrap: 4,
|
||||
Unwrap: 5,
|
||||
WithdrawNative: 6,
|
||||
Withdraw: 7,
|
||||
}
|
||||
|
||||
export class Transaction {
|
||||
constructor(chainId, type) {
|
||||
@@ -37,11 +22,18 @@ export class Transaction {
|
||||
}
|
||||
|
||||
submit() {
|
||||
useWalletStore().transaction = this
|
||||
ensureVault()
|
||||
console.log('submitting transaction', this.type)
|
||||
const ws = useWalletStore();
|
||||
if ( ws.transaction !== null ) {
|
||||
console.error('Transaction already in progress', ws.transaction)
|
||||
return
|
||||
}
|
||||
ws.transaction = this
|
||||
}
|
||||
|
||||
// "propose" means attach the transaction to a specific vault
|
||||
propose(owner, vault) {
|
||||
console.log('transaction bind', owner, vault)
|
||||
if (this.vault !== null && this.vault !== vault) {
|
||||
this.failed('proposed vault did not match withdrawl vault', vault, this.vault)
|
||||
return
|
||||
@@ -128,10 +120,16 @@ export class Transaction {
|
||||
this.failed('vault contract was null while sending order transaction')
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const tx = toRaw(await this.createTx(contract))
|
||||
this.signed(tx)
|
||||
console.log(`sent transaction`, tx)
|
||||
tx.wait().then(this.mined.bind(this)).catch(this.failed.bind(this))
|
||||
console.log(`sent transaction`, tx)
|
||||
}
|
||||
catch (e) {
|
||||
this.failed(e)
|
||||
return null
|
||||
}
|
||||
return this.tx
|
||||
}
|
||||
|
||||
@@ -159,7 +157,22 @@ export class PlaceOrderTransaction extends Transaction {
|
||||
|
||||
|
||||
async createTx(vaultContract) {
|
||||
this.fee = await placementFee(this.vault, this.order)
|
||||
const tries = 65;
|
||||
let i;
|
||||
let success = false
|
||||
for (i=0; !success && i<tries; i++ ) {
|
||||
try {
|
||||
console.error('getting placement fee', vaultContract, this.order)
|
||||
this.fee = await placementFee(vaultContract, this.order)
|
||||
success = true
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('failed to get placement fee', e)
|
||||
await sleep(1000)
|
||||
}
|
||||
}
|
||||
if (!success)
|
||||
throw Error('failed to get placement fee')
|
||||
console.log('placing order', this.id, this.fee, this.order)
|
||||
return await vaultContract.placeDexorder(this.order, {value: this.fee.reduce((a, b) => a + b)})
|
||||
}
|
||||
@@ -169,36 +182,14 @@ export class PlaceOrderTransaction extends Transaction {
|
||||
super.end(state)
|
||||
if (state === TransactionState.Mined) {
|
||||
useChartOrderStore().resetOrders()
|
||||
nav('Status')
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
router.push({name: 'Status'})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// todo move to orderlib
|
||||
async function placementFee(vault, order, window = 300) {
|
||||
// If the fees are about to change within `window` seconds of now, we send the higher native amount of the two fees.
|
||||
// If the fees sent are too much, the vault will refund the sender.
|
||||
const v = await vaultContract(vault, provider)
|
||||
const feeManagerAddr = await v.feeManager()
|
||||
const feeManager = await newContract(feeManagerAddr, 'IFeeManager', provider)
|
||||
const [sched, changeTimestamp] = await Promise.all([feeManager.fees(), feeManager.proposedFeeActivationTime()])
|
||||
console.log('sched', order, sched)
|
||||
// single order placement selector
|
||||
const placementFeeSelector = 'placementFee((address,address,(uint8,uint24),uint256,uint256,bool,bool,bool,uint64,(uint16,bool,bool,bool,bool,bool,bool,bool,bool,uint16,uint24,uint32,uint32,(uint32,uint32),(uint32,uint32))[]),(uint8,uint8,uint8,uint8,uint8))'
|
||||
let [orderFee, gasFee] = await v[placementFeeSelector](order, [...sched])
|
||||
console.log('placementFee', orderFee, gasFee)
|
||||
if (Number(changeTimestamp) - timestamp() < window) {
|
||||
const nextSched = await feeManager.proposedFees()
|
||||
const [nextOrderFee, nextGasFee] = await v[placementFeeSelector](order, [...nextSched])
|
||||
if (nextOrderFee + nextGasFee > orderFee + gasFee)
|
||||
[orderFee, gasFee] = [nextOrderFee, nextGasFee]
|
||||
}
|
||||
return [orderFee, gasFee]
|
||||
}
|
||||
|
||||
|
||||
export class CancelOrderTransaction extends Transaction {
|
||||
constructor(chainId, index) {
|
||||
super(chainId, TransactionType.CancelOrder)
|
||||
@@ -277,4 +268,3 @@ export class UnwrapTransaction extends Transaction {
|
||||
return await vaultContract.unwrap(this.amount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
src/blockchain/transactionDecl.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export const TransactionState = {
|
||||
Created: 0, // user requested a transaction
|
||||
Proposed: 1, // tx is sent to the wallet
|
||||
Signed: 2, // tx is awaiting blockchain mining
|
||||
Rejected: 3, // user refused to sign the tx
|
||||
Error: 3, // unknown error sending the tx to the wallet
|
||||
Mined: 4, // transaction has been confirmed on-chain
|
||||
}
|
||||
export const TransactionType = {
|
||||
PlaceOrder: 1,
|
||||
CancelOrder: 2,
|
||||
CancelAll: 3,
|
||||
Wrap: 4,
|
||||
Unwrap: 5,
|
||||
WithdrawNative: 6,
|
||||
Withdraw: 7,
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
import {provider, setProvider} from "@/blockchain/provider.js";
|
||||
import {BrowserProvider, ethers} from "ethers";
|
||||
import {useStore} from "@/store/store";
|
||||
import {socket} from "@/socket.js";
|
||||
import {SingletonCoroutine} from "@/misc.js";
|
||||
import {errorSuggestsMissingVault, SingletonCoroutine} from "@/misc.js";
|
||||
import {newContract, vaultAddress, vaultContract} from "@/blockchain/contract.js";
|
||||
import {defineStore} from "pinia";
|
||||
import {ref} from "vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {metadataMap, version} from "@/version.js";
|
||||
import {CancelAllTransaction, TransactionState} from "@/blockchain/transaction.js";
|
||||
import {TransactionState, TransactionType} from "@/blockchain/transactionDecl.js";
|
||||
import {track} from "@/track.js";
|
||||
import {socket} from "@/socket.js";
|
||||
|
||||
|
||||
export let provider = null
|
||||
|
||||
|
||||
// DEPRECATED
|
||||
export const useWalletStore = defineStore('wallet', ()=>{
|
||||
// this is what the wallet is logged into. it could be different than the application's store.chainId.
|
||||
const chainId = ref(0)
|
||||
@@ -29,7 +27,26 @@ export const useWalletStore = defineStore('wallet', ()=>{
|
||||
const pendingOrders = ref([])
|
||||
|
||||
// NEW Format is a single Transaction class
|
||||
const transaction = ref(null)
|
||||
const _tx = ref(null)
|
||||
const transaction = computed({
|
||||
get() {return _tx.value},
|
||||
set(v) {
|
||||
_tx.value = v;
|
||||
if (v===null) {
|
||||
console.log('clear transaction')
|
||||
if (progressionInvoker!==null) {
|
||||
clearTimeout(progressionInvoker)
|
||||
progressionInvoker = null
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('set transaction', v)
|
||||
transactionProgressor.invoke();
|
||||
if (progressionInvoker===null)
|
||||
progressionInvoker = setInterval(()=>transactionProgressor.invoke(), 1000)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
chainId, pendingOrders, transaction,
|
||||
@@ -40,6 +57,7 @@ export const useWalletStore = defineStore('wallet', ()=>{
|
||||
export function onChainChanged(chainId) {
|
||||
console.log('onChainChanged', chainId)
|
||||
chainId = Number(chainId)
|
||||
socket.emit('chain', chainId)
|
||||
const store = useStore()
|
||||
const ws = useWalletStore()
|
||||
if( chainId !== ws.chainId ) {
|
||||
@@ -49,7 +67,7 @@ export function onChainChanged(chainId) {
|
||||
console.log('app chain changed', chainId)
|
||||
store.chainId = chainId
|
||||
store.account = null
|
||||
provider = new BrowserProvider(window.ethereum, chainId)
|
||||
setProvider(new BrowserProvider(window.ethereum, chainId))
|
||||
updateAccounts(chainId, provider)
|
||||
}
|
||||
else {
|
||||
@@ -73,10 +91,14 @@ function changeAccounts(chainId, accounts) {
|
||||
const addr = accounts[0]
|
||||
if (addr !== store.account) {
|
||||
console.log('account logged in', addr)
|
||||
track('login', {chainId, address: addr})
|
||||
store.account = addr
|
||||
store.vaults = []
|
||||
// one of these two methods will call flushTransactions()
|
||||
if (useWalletStore().transaction!==null)
|
||||
ensureVault()
|
||||
else
|
||||
discoverVaults(addr)
|
||||
flushTransactions()
|
||||
socket.emit('address', chainId, addr)
|
||||
}
|
||||
}
|
||||
@@ -99,17 +121,17 @@ export function detectChain() {
|
||||
try {
|
||||
window.ethereum.on('chainChanged', onChainChanged);
|
||||
window.ethereum.on('accountsChanged', onAccountsChanged);
|
||||
}
|
||||
catch (e) {
|
||||
console.log('Could not connect change hooks to wallet', e)
|
||||
return
|
||||
}
|
||||
new ethers.BrowserProvider(window.ethereum).getNetwork().then((network)=>{
|
||||
const chainId = network.chainId
|
||||
onChainChanged(chainId)
|
||||
})
|
||||
}
|
||||
catch (e) {
|
||||
console.log('Could not connect change hooks to wallet', e)
|
||||
}
|
||||
}
|
||||
|
||||
detectChain()
|
||||
|
||||
const errorHandlingProxy = {
|
||||
get(target, prop, proxy) {
|
||||
@@ -153,7 +175,16 @@ export async function connectWallet(chainId) {
|
||||
await updateAccounts(chainId, p)
|
||||
}
|
||||
catch (e) {
|
||||
if (e.reason!=='rejected') {
|
||||
console.log('connectWallet error', e.reason, e)
|
||||
if (e.reason==='rejected') {
|
||||
const ws = useWalletStore();
|
||||
const tx = ws.transaction
|
||||
if (tx) {
|
||||
tx.state = TransactionState.Rejected
|
||||
ws.transaction = null
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error(e, e.reason)
|
||||
throw e
|
||||
}
|
||||
@@ -171,6 +202,7 @@ function discoverVaults(owner) {
|
||||
}
|
||||
|
||||
const doDiscoverVaults = new SingletonCoroutine(_discoverVaults, 50)
|
||||
|
||||
async function _discoverVaults(owner) {
|
||||
const result = []
|
||||
const versions = []
|
||||
@@ -187,7 +219,6 @@ async function _discoverVaults(owner) {
|
||||
// console.log(`vault ${num} at`, addr)
|
||||
if (addr === null) // no more vaults
|
||||
break
|
||||
console.log('provider', provider)
|
||||
if (!provider) {
|
||||
console.log('No provider')
|
||||
return // do not change whatever was already found
|
||||
@@ -199,20 +230,21 @@ async function _discoverVaults(owner) {
|
||||
result.push(addr)
|
||||
versions.push(version)
|
||||
} catch (e) {
|
||||
if (e.value === '0x' && e.code === 'BAD_DATA' || e.revert === null && e.code === 'CALL_EXCEPTION')
|
||||
if (errorSuggestsMissingVault(e))
|
||||
console.log(`no vault ${num} at ${addr}`)
|
||||
else
|
||||
console.error(`discoverVaults failed`, e)
|
||||
return // do not change what was already found todo is this correct?
|
||||
}
|
||||
}
|
||||
console.log('new account === owner?', s.account, owner)
|
||||
if( s.account === owner ) { // double-check the account since it could have changed during our await
|
||||
s.vaults = result
|
||||
s.vaultVersions = versions
|
||||
if( useWalletStore().transaction ) {
|
||||
const num = 0 // todo multiple vaults
|
||||
if (result.length)
|
||||
flushOrders(s.chainId, owner, num, result[0])
|
||||
flushWalletTransactions(s.chainId, owner, num, result[0])
|
||||
else
|
||||
ensureVault2(s.chainId, owner, num)
|
||||
}
|
||||
@@ -254,7 +286,7 @@ async function doEnsureVault(chainId, owner, num) {
|
||||
if (s.vaults.length <= num)
|
||||
await _discoverVaults(owner)
|
||||
if( s.vaults[num] )
|
||||
flushOrders(chainId, owner, num, s.vaults[num])
|
||||
flushWalletTransactions(chainId, owner, num, s.vaults[num])
|
||||
else {
|
||||
console.log(`requesting vault ${owner} ${num}`)
|
||||
socket.emit('ensureVault', chainId, owner, num)
|
||||
@@ -276,15 +308,68 @@ export async function cancelOrder(vault, orderIndex) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function cancelAll(vault) {
|
||||
new CancelAllTransaction(useStore().chainId, vault).submit()
|
||||
async function progressTransactions() {
|
||||
const s = useStore()
|
||||
const ws = useWalletStore();
|
||||
console.log('progressTransactions', ws.transaction)
|
||||
if( ws.transaction===null )
|
||||
return
|
||||
if( s.account === null ) {
|
||||
let signer = null
|
||||
try {
|
||||
console.log('account is null. requesting sign-in.')
|
||||
signer = await provider.getSigner()
|
||||
}
|
||||
catch (e) {
|
||||
console.log('signer error', e.code, e.info?.error?.code)
|
||||
if (e?.info?.error?.code === 4001) {
|
||||
console.log('signer rejected')
|
||||
signer = null
|
||||
}
|
||||
else
|
||||
throw e
|
||||
}
|
||||
if (signer === null) {
|
||||
console.log('setting tx state to rejected')
|
||||
ws.transaction.state = TransactionState.Rejected
|
||||
ws.transaction = null
|
||||
return
|
||||
}
|
||||
}
|
||||
if( s.vault === null ) {
|
||||
console.log('vault is null. requesting vault creation.')
|
||||
ensureVault()
|
||||
return
|
||||
}
|
||||
if( ws.transaction.state < TransactionState.Proposed )
|
||||
ws.transaction.propose(s.account, s.vault)
|
||||
if( ws.transaction.type === TransactionType.PlaceOrder ) {
|
||||
flushWalletTransactions(s.chainId, s.account, 0, s.vault)
|
||||
}
|
||||
else {
|
||||
console.log('flushing transaction', ws.transaction.type)
|
||||
if (ws.transaction.state < TransactionState.Proposed) {
|
||||
pendTransaction(async (signer) => {
|
||||
if (signer.address !== ws.transaction.owner) {
|
||||
console.error('signer address does not match transaction owner', signer.address, ws.transaction.owner)
|
||||
return
|
||||
}
|
||||
const contract = await vaultContract(ws.transaction.vault, signer)
|
||||
return await ws.transaction.createTx(contract)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function flushOrders(chainId, owner, num, vault) {
|
||||
const transactionProgressor = new SingletonCoroutine(progressTransactions, 10)
|
||||
|
||||
let progressionInvoker = null
|
||||
|
||||
|
||||
export function flushWalletTransactions(chainId, owner, num, vault) {
|
||||
const ws = useWalletStore();
|
||||
if (ws.transaction!==null && ws.transaction.state < TransactionState.Proposed)
|
||||
ws.transaction.propose(owner, vault)
|
||||
let needsFlush = false
|
||||
console.log('flushWalletTransactions', chainId, owner, num, vault)
|
||||
let needsFlush = ws.transaction !== null && ws.transaction.type !== TransactionType.PlaceOrder
|
||||
for( const pend of ws.pendingOrders ) {
|
||||
if (pend.vault === null)
|
||||
pend.vault = vault
|
||||
@@ -353,6 +438,7 @@ function pendOrderAsTransaction(pend) {
|
||||
|
||||
|
||||
export function pendTransaction(sender, errHandler) {
|
||||
console.log('pendTransaction')
|
||||
const s = useStore()
|
||||
s.transactionSenders.push([sender,errHandler])
|
||||
flushTransactions()
|
||||
@@ -367,13 +453,32 @@ export function flushTransactions() {
|
||||
|
||||
async function asyncFlushTransactions() {
|
||||
const s = useStore()
|
||||
const ws = useWalletStore()
|
||||
console.log('flushTransactions', ws.transaction, s.vault)
|
||||
if (ws.transaction !== null) {
|
||||
if (s.vault === null) {
|
||||
console.log('transaction doesn\'t have a vault. creating one.')
|
||||
await ensureVault()
|
||||
if (s.vault === null) {
|
||||
console.error('vault could not be created')
|
||||
const tx = ws.transaction
|
||||
if (tx) {
|
||||
tx.state = TransactionState.Error
|
||||
ws.transaction = null
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if( provider === null ) {
|
||||
console.log('warning: asyncFlushOrders() cancelled due to null provider')
|
||||
return
|
||||
}
|
||||
const senders = s.transactionSenders
|
||||
if (!senders.length)
|
||||
if (!senders.length) {
|
||||
console.log('no transactionSenders!')
|
||||
return
|
||||
}
|
||||
console.log(`flushing ${senders.length} transactions`)
|
||||
let signer
|
||||
try {
|
||||
@@ -489,7 +594,7 @@ const _chainInfos = {
|
||||
1337: {
|
||||
"chainId": "0x539",
|
||||
"chainName": "Dexorder Alpha Testnet",
|
||||
"rpcUrls": ["https://rpc.alpha.dexorder.trade"],
|
||||
"rpcUrls": ["https://rpc.alpha.dexorder.com"],
|
||||
"nativeCurrency": {
|
||||
"name": "Test Ethereum",
|
||||
"symbol": "TETH",
|
||||
@@ -531,3 +636,26 @@ export async function addNetwork(chainId) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function addNetworkAndConnectWallet(chainId) {
|
||||
try {
|
||||
await switchChain(chainId)
|
||||
} catch (e) {
|
||||
if (e.code === 4001) {
|
||||
// explicit user rejection
|
||||
return
|
||||
} else if (e.code === 4902) {
|
||||
try {
|
||||
await addNetwork(chainId)
|
||||
} catch (e) {
|
||||
console.log(`Could not add network ${chainId}`)
|
||||
}
|
||||
} else
|
||||
console.log('switchChain() failure', e)
|
||||
}
|
||||
try {
|
||||
await connectWallet(chainId)
|
||||
} catch (e) {
|
||||
if (e.code !== 4001)
|
||||
console.log('connectWallet() failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,3 +53,7 @@ export function dirtyItems(a, b) {
|
||||
result[k] = b[k]
|
||||
return result
|
||||
}
|
||||
|
||||
export function copyPoints(points) {
|
||||
return points.map((p)=>({time: p.time, price: p.price}))
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import {invokeCallbacks, prototype} from "@/common.js";
|
||||
import {DataFeed, feelessTickerKey, getAllSymbols, lookupSymbol} from "@/charts/datafeed.js";
|
||||
import {intervalToSeconds, SingletonCoroutine} from "@/misc.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {DataFeed, defaultSymbol, feelessTickerKey, getAllSymbols, lookupSymbol} from "@/charts/datafeed.js";
|
||||
import {intervalToSeconds, secondsToInterval, SingletonCoroutine, toHuman, toPrecision} from "@/misc.js";
|
||||
import {usePrefStore, useStore} from "@/store/store.js";
|
||||
import {tvCustomThemes} from "../../theme.js";
|
||||
|
||||
export let widget = null
|
||||
export let chart = null
|
||||
export let crosshairPoint = null
|
||||
export let defaultShapeHandler = null // if set, then TV events that dont have a registered shape handler get passed directly to this function
|
||||
let symbolChangedCbs = [] // callbacks for TV's chart.onSymbolChanged()
|
||||
|
||||
|
||||
const s = useStore()
|
||||
const co = useChartOrderStore()
|
||||
const prefs = usePrefStore()
|
||||
|
||||
export function addSymbolChangedCallback(cb) {
|
||||
symbolChangedCbs.push(cb)
|
||||
@@ -23,11 +25,13 @@ export function removeSymbolChangedCallback(cb) {
|
||||
}
|
||||
|
||||
function symbolChanged(symbol) {
|
||||
const info = symbol===null ? null : lookupSymbol(symbol.ticker)
|
||||
const info = symbol===null ? (defaultSymbol===null?'default':defaultSymbol) : lookupSymbol(symbol.ticker)
|
||||
co.selectedSymbol = info
|
||||
// console.log('setting prefs ticker', info.ticker)
|
||||
prefs.selectedTicker = info.ticker
|
||||
symbolChangedCbs.forEach((cb) => cb(info))
|
||||
updateFeeDropdown()
|
||||
console.log('symbol changed', info)
|
||||
// console.log('symbol changed', info)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,15 +57,24 @@ export async function setSymbolTicker(ticker) {
|
||||
}
|
||||
|
||||
|
||||
function changeInterval(interval, _timeframe) {
|
||||
co.intervalSecs = intervalToSeconds(interval)
|
||||
DataFeed.intervalChanged(co.intervalSecs)
|
||||
function changeInterval(interval) {
|
||||
const secs = intervalToSeconds(interval)
|
||||
co.intervalSecs = secs
|
||||
prefs.selectedTimeframe = interval
|
||||
DataFeed.intervalChanged(secs)
|
||||
}
|
||||
|
||||
|
||||
export function changeIntervalSecs(secs) {
|
||||
const interval = secondsToInterval(secs);
|
||||
co.intervalSecs = secs
|
||||
prefs.selectedTimeframe = interval
|
||||
DataFeed.intervalChanged(secs)
|
||||
}
|
||||
|
||||
function dataLoaded() {
|
||||
const range = chartMeanRange()
|
||||
console.log('new mean range', range,)
|
||||
// console.log('new mean range', range,)
|
||||
co.meanRange = range
|
||||
}
|
||||
|
||||
@@ -79,54 +92,75 @@ const subscribeEvents = [
|
||||
*/
|
||||
|
||||
|
||||
let feeDropdown = null
|
||||
let poolButtonTextElement = null
|
||||
|
||||
function initFeeDropdown() {
|
||||
const button = widget.createButton()
|
||||
button.setAttribute('title', 'See Pool Info and Choose Fee');
|
||||
button.addEventListener('click', function () {
|
||||
co.showPoolSelection = true
|
||||
});
|
||||
button.id = 'pool-button'
|
||||
|
||||
button.style.height = '34px';
|
||||
button.style.display = 'flex';
|
||||
button.style.alignItems = 'center';
|
||||
button.addEventListener('mouseover', () => {
|
||||
button.style.backgroundColor = 'rgb(60,60,60)';
|
||||
});
|
||||
button.addEventListener('mouseout', () => {
|
||||
button.style.backgroundColor = '';
|
||||
});
|
||||
button.style.margin = '2px 0';
|
||||
button.style.borderRadius = '4px';
|
||||
button.classList.add('pool-button')
|
||||
|
||||
let img = document.createElement('img');
|
||||
img.src = '/arbitrum-logo.svg';
|
||||
img.style.width = '1em';
|
||||
img.style.marginRight = '0.2em';
|
||||
button.appendChild(img);
|
||||
|
||||
img = document.createElement('img');
|
||||
img.src = '/uniswap-logo.svg';
|
||||
img.style.height = '1.25em';
|
||||
img.style.marginRight = '0.2em';
|
||||
img.style.backgroundColor = 'white';
|
||||
img.style.borderRadius = '50%';
|
||||
button.appendChild(img);
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.style.marginY = 'auto';
|
||||
button.appendChild(span);
|
||||
poolButtonTextElement = span
|
||||
|
||||
export function initFeeDropdown(w) {
|
||||
widget = w
|
||||
widget.createDropdown(
|
||||
{
|
||||
title: 'Fees',
|
||||
tooltip: 'Choose Fee Tier',
|
||||
items: [/*{title: 'Automatic Fee Selection', onSelect: () => {log('autofees')}}*/],
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"><g fill="none" stroke="currentColor"><circle cx="10" cy="10" r="2.5"/><circle cx="18" cy="18" r="2.5"/><path stroke-linecap="square" d="M17.5 7.5l-7 13"/></g></svg>`,
|
||||
}
|
||||
).then(dropdown => {
|
||||
feeDropdown = dropdown;
|
||||
updateFeeDropdown()
|
||||
})
|
||||
}
|
||||
|
||||
export function updateFeeDropdown() {
|
||||
if (feeDropdown === null) return
|
||||
if (poolButtonTextElement===null) return
|
||||
const symbolItem = useChartOrderStore().selectedSymbol
|
||||
let items
|
||||
if (symbolItem === null)
|
||||
items = [{title: '0.00%'}]
|
||||
else {
|
||||
const feeGroup = symbolItem.feeGroup
|
||||
items = feeGroup.map((p) => {
|
||||
const [_addr, fee] = p
|
||||
return {
|
||||
title: (fee / 10000).toFixed(2) + '%',
|
||||
onSelect: ()=>{
|
||||
if (fee !== symbolItem.fee)
|
||||
selectPool(fee)
|
||||
},
|
||||
let text = ''
|
||||
text += (symbolItem.fee / 10000).toFixed(2) + '%'
|
||||
const index = symbolItem.feeGroup.findIndex((p) => p[1] === symbolItem.fee)
|
||||
if (symbolItem.liquiditySymbol) {
|
||||
const liq = symbolItem.liquidities[index]
|
||||
if (symbolItem.liquiditySymbol === 'USD')
|
||||
text += ` $${toHuman(liq)}`
|
||||
else
|
||||
text = ` ${toHuman(liq)} ${symbolItem.liquiditySymbol}`
|
||||
}
|
||||
})
|
||||
}
|
||||
feeDropdown.applyOptions({items})
|
||||
poolButtonTextElement.textContent = text
|
||||
}
|
||||
|
||||
function selectPool(fee) {
|
||||
const co = useChartOrderStore();
|
||||
const s = co.selectedSymbol;
|
||||
const ticker = feelessTickerKey(s.ticker) + '|' + fee
|
||||
if (ticker !== s.ticker)
|
||||
setSymbolTicker(ticker).catch((e)=>console.error('Could not change TV symbol to', ticker))
|
||||
export function initTVButtons() {
|
||||
initFeeDropdown();
|
||||
}
|
||||
|
||||
export function initWidget(el) {
|
||||
getAllSymbols()
|
||||
const symbol = prefs.selectedTicker === null ? 'default' : prefs.selectedTicker
|
||||
const interval = prefs.selectedTimeframe === null ? '15' : prefs.selectedTimeframe
|
||||
widget = window.tvWidget = new TradingView.widget({
|
||||
|
||||
// Widget Options
|
||||
@@ -134,21 +168,22 @@ export function initWidget(el) {
|
||||
library_path: "/charting_library/",
|
||||
// debug: true,
|
||||
autosize: true,
|
||||
symbol: 'default',
|
||||
interval: '15',
|
||||
symbol,
|
||||
interval,
|
||||
container: el,
|
||||
datafeed: DataFeed, // use this for ohlc
|
||||
locale: "en",
|
||||
disabled_features: [],
|
||||
enabled_features: ['saveload_separate_drawings_storage'],
|
||||
drawings_access: {type: 'white', tools: [],}, // show no tools
|
||||
disabled_features: ['main_series_scale_menu','display_market_status',],
|
||||
enabled_features: ['saveload_separate_drawings_storage','snapshot_trading_drawings','show_exchange_logos','show_symbol_logos',],
|
||||
// drawings_access: {type: 'white', tools: [],}, // show no tools
|
||||
custom_themes: tvCustomThemes,
|
||||
theme: useStore().theme,
|
||||
timezone: prefs.timezone,
|
||||
|
||||
// Chart Overrides
|
||||
// https://www.tradingview.com/charting-library-docs/latest/customization/overrides/chart-overrides
|
||||
overrides: {
|
||||
// "mainSeriesProperties.priceAxisProperties.log": false,
|
||||
"mainSeriesProperties.priceAxisProperties.log": false,
|
||||
}
|
||||
|
||||
});
|
||||
@@ -161,14 +196,24 @@ export function initWidget(el) {
|
||||
widget.subscribe('onSelectedLineToolChanged', onSelectedLineToolChanged)
|
||||
widget.subscribe('mouse_down', mouseDown)
|
||||
widget.subscribe('mouse_up', mouseUp)
|
||||
widget.headerReady().then(()=>initFeeDropdown(widget))
|
||||
widget.headerReady().then(()=>initTVButtons())
|
||||
widget.onChartReady(initChart)
|
||||
console.log('tv widget initialized')
|
||||
}
|
||||
|
||||
|
||||
export function onChartReady(f) {
|
||||
if (co.chartReady)
|
||||
f(widget, chart)
|
||||
else
|
||||
chartInitCbs.push(f)
|
||||
}
|
||||
|
||||
let chartInitCbs = []
|
||||
|
||||
|
||||
function initChart() {
|
||||
console.log('init chart')
|
||||
// console.log('init chart')
|
||||
chart = widget.activeChart()
|
||||
const themeName = useStore().theme;
|
||||
widget.changeTheme(themeName).catch((e)=>console.warn(`Could not change theme to ${themeName}`, e))
|
||||
@@ -191,7 +236,12 @@ function initChart() {
|
||||
}
|
||||
changeInterval(widget.symbolInterval().interval)
|
||||
co.chartReady = true
|
||||
console.log('chart ready')
|
||||
setTimeout(()=>{
|
||||
for (const cb of chartInitCbs)
|
||||
cb(widget, chart)
|
||||
chartInitCbs = []
|
||||
}, 1)
|
||||
// console.log('chart ready')
|
||||
}
|
||||
|
||||
|
||||
@@ -236,13 +286,14 @@ let drawingCallbacks = null
|
||||
|
||||
export function drawShape(shapeType, ...callbacks) {
|
||||
// puts the chart into a line-drawing mode for a new shape
|
||||
console.log('drawShape', callbacks, shapeType.name, shapeType.code)
|
||||
// console.log('drawShape', callbacks, shapeType.name, shapeType.code)
|
||||
if( drawingCallbacks )
|
||||
invokeCallbacks(drawingCallbacks, 'onUndraw')
|
||||
drawingCallbacks = callbacks
|
||||
drawingTool = null
|
||||
previousDrawingTool = widget.selectedLineTool()
|
||||
co.drawing = true
|
||||
co.drew = false
|
||||
widget.selectLineTool(shapeType.code)
|
||||
invokeCallbacks(callbacks, 'onDraw')
|
||||
}
|
||||
@@ -301,7 +352,7 @@ const shapeCallbacks = {}
|
||||
|
||||
function onSelectedLineToolChanged() {
|
||||
const tool = widget.selectedLineTool();
|
||||
console.log('line tool changed', tool)
|
||||
// console.log('line tool changed', tool)
|
||||
if (drawingTool===null)
|
||||
drawingTool = tool
|
||||
else if (tool!==drawingTool && co.drawing)
|
||||
@@ -351,9 +402,9 @@ function doHandleCrosshairMovement(point) {
|
||||
}
|
||||
const points = structuredClone(shape.getPoints());
|
||||
const lpbe = shape._model._linePointBeingEdited
|
||||
points[lpbe] = point
|
||||
// console.log('drag calling onPoints', points, shape, lpbe)
|
||||
invokeCallbacks(shapeCallbacks[shapeId], 'onPoints', shapeId, shape, points)
|
||||
points[lpbe===null?0:lpbe] = point
|
||||
// console.log('calling onDrag', points, shape)
|
||||
invokeCallbacks(shapeCallbacks[shapeId], 'onDrag', shapeId, shape, points)
|
||||
}
|
||||
}
|
||||
else if (draggingShapeIds.length > 0) {
|
||||
@@ -437,9 +488,11 @@ function doHandleDrawingEvent(id, event) {
|
||||
const props = shape.getProperties()
|
||||
if (id in shapeCallbacks)
|
||||
invokeCallbacks(shapeCallbacks[id], 'onProps', id, shape, props)
|
||||
else
|
||||
// otherwise it's an event on a shape we don't "own"
|
||||
else {
|
||||
// otherwise it's an event on a shape we don't "own" that could be being drawn
|
||||
co.drew = true
|
||||
console.log('warning: ignoring setProperties on TV shape', id, props)
|
||||
}
|
||||
} else if (event === 'move') {
|
||||
if (id in shapeCallbacks) {
|
||||
invokeCallbacks(shapeCallbacks[id], 'onMove', id, shape)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {provider} from "@/blockchain/provider.js";
|
||||
import {convertTvResolution, loadOHLC} from './ohlc.js';
|
||||
import {metadata} from "@/version.js";
|
||||
import FlexSearch from "flexsearch";
|
||||
@@ -5,8 +6,10 @@ import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {subOHLC, unsubOHLC} from "@/blockchain/ohlcs.js";
|
||||
import {ohlcStart} from "@/charts/chart-misc.js";
|
||||
import {timestamp, withTimeout} from "@/common.js";
|
||||
import {erc20Contract} from "@/blockchain/contract.js";
|
||||
import {track} from "@/track.js";
|
||||
|
||||
import {timestamp} from "@/common.js";
|
||||
|
||||
const DEBUG_LOGGING = false
|
||||
const log = DEBUG_LOGGING ? console.log : ()=>{}
|
||||
@@ -61,7 +64,7 @@ const configurationData = {
|
||||
value: 'UNIv3',
|
||||
name: 'Uniswap v3',
|
||||
desc: 'Uniswap v3',
|
||||
logo: 'https://upload.wikimedia.org/wikipedia/commons/e/e7/Uniswap_Logo.svg',
|
||||
logo: '/uniswap-logo.svg',
|
||||
},
|
||||
],
|
||||
// The `symbols_types` arguments are used for the `searchSymbols` method if a user selects this symbol type
|
||||
@@ -108,8 +111,10 @@ export function feelessTickerKey(ticker) {
|
||||
|
||||
function addSymbol(chainId, p, base, quote, inverted) {
|
||||
const symbol = base.s + '/' + quote.s
|
||||
const fee = `${(p.f/10000).toFixed(2)}%`
|
||||
const exchange = ['Uniswap v2', 'Uniswap v3'][p.e] + ' ' + fee
|
||||
// const fee = `${(p.f/10000).toFixed(2)}%`
|
||||
// const exchange = ['Uniswap v2', 'Uniswap v3'][p.e] + ' ' + fee
|
||||
const exchange = ['Uniswap v2', 'Uniswap v3'][p.e]
|
||||
const exchange_logo = '/uniswap-logo.svg'
|
||||
const full_name = exchange + ':' + symbol // + '%' + formatFee(fee)
|
||||
const ticker = tickerKey(chainId, p.e, base.a, quote.a, p.f)
|
||||
// add the search index only if this is the natural, noninverted base/quote pair
|
||||
@@ -120,7 +125,7 @@ function addSymbol(chainId, p, base, quote, inverted) {
|
||||
const symbolInfo = {
|
||||
key: ticker, ticker,
|
||||
chainId, address: p.a, exchangeId: p.e,
|
||||
full_name, symbol, description, exchange, type, inverted, base, quote, decimals, x:p.x, fee:p.f,
|
||||
full_name, symbol, description, exchange, exchange_logo, type, inverted, base, quote, decimals, x:p.x, fee:p.f,
|
||||
};
|
||||
_symbols[ticker] = symbolInfo
|
||||
const feelessKey = feelessTickerKey(ticker)
|
||||
@@ -131,8 +136,13 @@ function addSymbol(chainId, p, base, quote, inverted) {
|
||||
else
|
||||
feeGroups[feelessKey] = [[symbolInfo.address, symbolInfo.fee]]
|
||||
symbolInfo.feeGroup = feeGroups[feelessKey]
|
||||
if (defaultSymbol===null && !invertedDefault(symbolInfo.base.a, symbolInfo.quote.a))
|
||||
// if (defaultSymbol===null) {
|
||||
// console.log(`invertedDefault(${symbolInfo.base.s}, ${symbolInfo.quote.s})`,invertedDefault(symbolInfo.base.a, symbolInfo.quote.a))
|
||||
// }
|
||||
if (defaultSymbol===null && !invertedDefault(symbolInfo.base.a, symbolInfo.quote.a)) {
|
||||
console.log('setting default symbol', symbolInfo.base.s, symbolInfo.quote.s, symbolInfo.base.a, symbolInfo.quote.a)
|
||||
defaultSymbol = _symbols[ticker]
|
||||
}
|
||||
log('new symbol', ticker, _symbols[ticker])
|
||||
}
|
||||
|
||||
@@ -325,6 +335,14 @@ class RealtimeSubscription {
|
||||
}
|
||||
|
||||
|
||||
async function getLiquidities(markToken, symbolItem) {
|
||||
const token = await erc20Contract(markToken.a, provider)
|
||||
const liquidities = await Promise.all(symbolItem.feeGroup.map(
|
||||
async ([addr, fee]) => await token.balanceOf(addr)
|
||||
))
|
||||
return liquidities;
|
||||
}
|
||||
|
||||
export const DataFeed = {
|
||||
onReady(callback) {
|
||||
log('[onReady]: Method call');
|
||||
@@ -349,6 +367,8 @@ export const DataFeed = {
|
||||
result.push(_symbols[ticker])
|
||||
seen[ticker] = true
|
||||
}
|
||||
if (userInput.length>=3)
|
||||
track('search', {search_term: userInput})
|
||||
onResultReadyCallback(result);
|
||||
},
|
||||
|
||||
@@ -369,22 +389,59 @@ export const DataFeed = {
|
||||
onResolveErrorCallback,
|
||||
extension
|
||||
) {
|
||||
log('[resolveSymbol]: Method call', symbolName);
|
||||
console.log('resolveSymbol', symbolName);
|
||||
const symbols = getAllSymbols();
|
||||
const symbolItem = symbolName === 'default' ? defaultSymbol : symbols[symbolName]
|
||||
if (symbolName==='default') {
|
||||
console.log('using default symbol', defaultSymbol)
|
||||
}
|
||||
if (!symbolItem) {
|
||||
log('[resolveSymbol]: Cannot resolve symbol', symbolName);
|
||||
console.log('[resolveSymbol]: Cannot resolve symbol', symbolName);
|
||||
onResolveErrorCallback('cannot resolve symbol');
|
||||
return;
|
||||
}
|
||||
const co = useChartOrderStore();
|
||||
co.selectedSymbol = symbolItem
|
||||
const feelessKey = feelessTickerKey(symbolItem.ticker)
|
||||
const symbolsByFee = feeGroups[feelessKey]
|
||||
symbolsByFee.sort((a,b)=>a.fee-b.fee)
|
||||
const pool = symbolsByFee[Math.floor((symbolsByFee.length - 1)/2)] // median rounded down
|
||||
// noinspection JSValidateTypes
|
||||
co.selectedPool = pool // todo remove
|
||||
|
||||
let ticker = symbolItem.ticker
|
||||
try {
|
||||
if (!symbolItem.liquiditySymbol) {
|
||||
// fetch liquidities and cache on the symbolItem
|
||||
const inv = invertedDefault(symbolItem.base.a, symbolItem.quote.a)
|
||||
const markToken = inv ? symbolItem.base : symbolItem.quote
|
||||
const mark = useStore().markPrice(markToken.a)
|
||||
const liquidities = await withTimeout(
|
||||
getLiquidities(markToken, symbolItem),
|
||||
3000,
|
||||
'liquidity fetch timeout'
|
||||
)
|
||||
symbolItem.liquidities = liquidities.map(l => Number(l / 10n ** BigInt(markToken.d)))
|
||||
if (mark) {
|
||||
symbolItem.liquidities = symbolItem.liquidities.map(l => l * mark)
|
||||
symbolItem.liquiditySymbol = 'USD'
|
||||
} else {
|
||||
symbolItem.liquiditySymbol = symbolItem.quote.s
|
||||
}
|
||||
}
|
||||
const liqsAndFees = []
|
||||
for (let i=0; i<symbolItem.feeGroup.length; i++) {
|
||||
const [addr, fee] = symbolItem.feeGroup[i]
|
||||
const liq = symbolItem.liquidities[i]
|
||||
liqsAndFees.push([liq, fee])
|
||||
if (fee === symbolItem.fee)
|
||||
symbolItem.liquidity = liq
|
||||
}
|
||||
liqsAndFees.sort((a,b) => b[0] - a[0])
|
||||
const highestLiquidityFee = liqsAndFees[0][1]
|
||||
// console.log('liquidities', liqsAndFees)
|
||||
// console.log('best liquidity', highestLiquidityFee, liqsAndFees[0][0], symbolItem.liquiditySymbol)
|
||||
ticker = feelessTickerKey(ticker) + '|' + highestLiquidityFee
|
||||
}
|
||||
catch (error) {
|
||||
// use the median fee group instead
|
||||
console.log('liquidity fetch error', error)
|
||||
}
|
||||
|
||||
// LibrarySymbolInfo
|
||||
// https://www.tradingview.com/charting-library-docs/latest/api/interfaces/Charting_Library.LibrarySymbolInfo
|
||||
const symbolInfo = {
|
||||
@@ -406,7 +463,7 @@ export const DataFeed = {
|
||||
// volume_precision: 2,
|
||||
data_status: 'streaming',
|
||||
};
|
||||
log('[resolveSymbol]: Symbol resolved', symbolName);
|
||||
// console.log('[resolveSymbol]: Symbol resolved', symbolName);
|
||||
onSymbolResolvedCallback(symbolInfo)
|
||||
},
|
||||
|
||||
@@ -684,4 +741,4 @@ export const DataFeed = {
|
||||
|
||||
|
||||
let _rolloverBumper = null
|
||||
let defaultSymbol = null
|
||||
export let defaultSymbol = null
|
||||
|
||||
@@ -52,7 +52,7 @@ function addDay(timestamp) {
|
||||
function addMonth(timestamp) {
|
||||
const date = new Date(timestamp*1000)
|
||||
const result = Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()) / 1000
|
||||
console.log('addMonth', timestamp, result, new Date(timestamp*1000), new Date(result*1000))
|
||||
// console.log('addMonth', timestamp, result, new Date(timestamp*1000), new Date(result*1000))
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ async function getUrl(url) {
|
||||
|
||||
|
||||
export async function loadOHLC (symbol, contract, from, to, tvRes) {
|
||||
console.log('loadOHLC', tvRes, new Date(1000*from), new Date(1000*to), symbol, contract);
|
||||
// console.log('loadOHLC', tvRes, new Date(1000*from), new Date(1000*to), symbol, contract);
|
||||
let chainId
|
||||
let bars = [];
|
||||
let inverted = false;
|
||||
@@ -179,7 +179,7 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
|
||||
if (response.length) {
|
||||
const [start,end,price] = response.split(',')
|
||||
seriesStarts[baseUrl] = parseInt(start)
|
||||
console.log(`Series ${baseUrl} starts at ${new Date(start*1000)}`)
|
||||
// console.log(`Series ${baseUrl} starts at ${new Date(start*1000)}`)
|
||||
}
|
||||
else {
|
||||
console.error(`Bad response while fetching ${baseUrl+'quote.csv'}`)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {DISTANT_FUTURE, DISTANT_PAST, MAX_FRACTION} from "@/blockchain/orderlib.js";
|
||||
import {allocationText, DLine, HLine} from "@/charts/shape.js";
|
||||
import {createShape, deleteShapeId} from "@/charts/chart.js";
|
||||
import {createShape, deleteShapeId, widget} from "@/charts/chart.js";
|
||||
import {sideColor} from "@/misc.js";
|
||||
import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import {timestamp} from "@/common.js";
|
||||
@@ -87,9 +87,11 @@ class TrancheShapes {
|
||||
price *= scale
|
||||
// console.log('price', price)
|
||||
const channel = buy?'low':'high';
|
||||
const text = (buy ? 'Buy ' : 'Sell ') + allocationText(weight, amount, amountSymbol, '\n')
|
||||
const s = createShape(buy?'arrow_up':'arrow_down', {time, price}, {channel,text,lock:true})
|
||||
// console.log('created fill shape at', time, price)
|
||||
const text = allocationText(buy, weight, amount, amountSymbol, amountIsBase ? null : this.symbol.base.s, 1, '\n')
|
||||
const color = sideColor(buy);
|
||||
const options = {channel,text,lock:true,overrides:{color: color, arrowColor: color}};
|
||||
const s = createShape(buy?'arrow_up':'arrow_down', {time, price}, options)
|
||||
console.log('created fill shape at', time, price, widget.activeChart().getShapeById(s).getProperties())
|
||||
this.fills.push(s)
|
||||
}
|
||||
|
||||
@@ -122,7 +124,7 @@ class TrancheShapes {
|
||||
// console.log('hline', price)
|
||||
const model = {
|
||||
price, breakout, color, extraText, textLocation,
|
||||
allocation, maxAllocation, amount, amountSymbol,
|
||||
allocation, maxAllocation, amount, amountSymbol, buy,
|
||||
}
|
||||
const s = new HLine(model, null, null, null, true)
|
||||
this.shapes.push(s)
|
||||
@@ -146,7 +148,7 @@ class TrancheShapes {
|
||||
extendLeft: t.startTime === DISTANT_PAST,
|
||||
extendRight: t.endTime === DISTANT_FUTURE,
|
||||
breakout, color, extraText, textLocation,
|
||||
allocation, maxAllocation, amount, amountSymbol,
|
||||
allocation, maxAllocation, amount, amountSymbol, buy,
|
||||
}
|
||||
const s = new DLine(model, null, null, null, true)
|
||||
this.shapes.push(s)
|
||||
|
||||
@@ -4,7 +4,7 @@ import {invokeCallback, mixin} from "@/common.js";
|
||||
import {chart, createShape, deleteShapeId, dragging, draggingShapeIds, drawShape, widget} from "@/charts/chart.js";
|
||||
import Color from "color";
|
||||
import {dirtyItems, dirtyPoints, nearestOhlcStart} from "@/charts/chart-misc.js";
|
||||
import {defined} from "@/misc.js";
|
||||
import {defined, toPrecision} from "@/misc.js";
|
||||
|
||||
|
||||
//
|
||||
@@ -36,29 +36,35 @@ export const ShapeType = {
|
||||
HLine: {name: 'Horizontal Line', code: 'horizontal_line', drawingProp: 'linetoolhorzline'},
|
||||
VLine: {name: 'Vertical Line', code: 'vertical_line', drawingProp: 'linetoolvertline'},
|
||||
PriceRange: {name: 'Price Range', code: 'price_range'},
|
||||
DateRange: {name: 'Date Range', code: 'date_range', drawingProp: 'linetooldaterange'},
|
||||
}
|
||||
|
||||
|
||||
export function allocationText(weight, amount, symbol, separator = ' ') {
|
||||
// set breakout=true for a buy breakout and breakout=false for a sell breakout
|
||||
export function allocationText(buy, weight, amount, baseSymbol, amountSymbol = null, parts = 1, separator = ' ') {
|
||||
const hasAmount = amount !== null && amount !== undefined && amount > 0
|
||||
if (hasAmount)
|
||||
amount = Number(amount)
|
||||
const hasWeight = weight !== null && weight !== undefined && weight !== 1
|
||||
if (hasWeight)
|
||||
weight = Number(weight)
|
||||
let text = ''
|
||||
let text = buy === undefined ? '' : buy ? 'Buy ' : 'Sell '
|
||||
if (hasWeight)
|
||||
text += `${(weight * 100).toFixed(1)}%`
|
||||
const hasSymbol = symbol !== null && symbol !== undefined
|
||||
const hasSymbol = baseSymbol !== null && baseSymbol !== undefined
|
||||
if (hasAmount && hasSymbol) {
|
||||
if (hasWeight)
|
||||
text += separator
|
||||
text += `${amount.toPrecision(3).toLocaleString('fullwide')} ${symbol}`
|
||||
if (amountSymbol!==null && amountSymbol!==baseSymbol)
|
||||
text += `${baseSymbol} worth ${toPrecision(amount,3)} ${amountSymbol}`
|
||||
else
|
||||
text += `${toPrecision(amount,3)} ${baseSymbol}`
|
||||
}
|
||||
if (parts > 1)
|
||||
text += separator + `in ${Math.round(parts)} parts`
|
||||
return text
|
||||
}
|
||||
|
||||
|
||||
export class Shape {
|
||||
|
||||
constructor(type, onModel=null, onDelete=null, props=null, readonly=false, overrides={}) {
|
||||
@@ -108,6 +114,7 @@ export class Shape {
|
||||
this.model.maxAllocation = null
|
||||
// both amount and amountSymbol must be set in order to display amount text
|
||||
this.model.amount = null
|
||||
this.model.baseSymbol = null
|
||||
this.model.amountSymbol = null
|
||||
this.model.extraText = null
|
||||
this.model.textLocation = null // defaults to 'above' if not set
|
||||
@@ -130,12 +137,16 @@ export class Shape {
|
||||
this.model.amount = model.amount
|
||||
if (defined(model.amountSymbol))
|
||||
this.model.amountSymbol = model.amountSymbol
|
||||
if (defined(model.baseSymbol))
|
||||
this.model.baseSymbol = model.baseSymbol
|
||||
if (defined(model.extraText))
|
||||
this.model.extraText = model.extraText
|
||||
if (defined(model.breakout))
|
||||
this.model.breakout = model.breakout
|
||||
if (defined(model.textLocation))
|
||||
this.model.textLocation = model.textLocation
|
||||
if (defined(model.buy))
|
||||
this.model.buy = model.buy
|
||||
|
||||
const newProps = {}
|
||||
|
||||
@@ -160,9 +171,9 @@ export class Shape {
|
||||
newProps.linecolor = color
|
||||
|
||||
// text label
|
||||
let text = allocationText(this.model.allocation, this.model.amount, this.model.amountSymbol)
|
||||
let text = allocationText(this.model.buy, this.model.allocation, this.model.amount, this.model.baseSymbol, this.model.amountSymbol)
|
||||
if (this.model.breakout)
|
||||
text += ' ' + (this.model.textLocation==='above' ? '▲Breakout▲' : '▼Breakout▼')
|
||||
text += ' ' + (this.model.textLocation==='above' ? '▲Breakout▲' : '▼Breakdown▼')
|
||||
if (this.model.extraText)
|
||||
text += ' '+this.model.extraText
|
||||
if (this.debug) text = `${this.id} ` + text
|
||||
@@ -320,14 +331,16 @@ export class Shape {
|
||||
}
|
||||
|
||||
// diagonals need to override this to adjust their price as well.
|
||||
pointsToTvOhlcStart(points, periodSeconds=null) {
|
||||
pointsToTvOhlcStart(points, periodSeconds = null) {
|
||||
return points === null ? null : points.map((p) => {
|
||||
return {time: nearestOhlcStart(p.time, periodSeconds), price: p.price}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onPoints(points) {} // the control points of an existing shape were changed
|
||||
|
||||
onDrag(points) { this.onPoints(points) }
|
||||
|
||||
setProps(props) {
|
||||
if (!props || Object.keys(props).length===0) return
|
||||
if (this.debug) console.log('setProps', this.id, props)
|
||||
@@ -377,7 +390,6 @@ export class Shape {
|
||||
onUndraw() {} // drawing was canceled by clicking on a different tool
|
||||
onAddPoint() {} // the user clicked a point while drawing (that point is added to the points list)
|
||||
onMove(points) {} // the shape was moved by dragging a drawing element not the control point
|
||||
onDrag(points) {}
|
||||
onHide(props) {}
|
||||
onShow(props) {}
|
||||
onClick() {} // the shape was selected
|
||||
@@ -472,16 +484,6 @@ class ShapeTVCallbacks {
|
||||
|
||||
|
||||
export class Line extends Shape {
|
||||
onDrag(points) {
|
||||
const s = this.tvShape();
|
||||
if (this.debug) {
|
||||
console.log('shape', s.id, s)
|
||||
console.log('currentMovingPoint', s._source.currentMovingPoint())
|
||||
console.log('startMovingPoint', s._source.startMovingPoint())
|
||||
console.log('isBeingEdited', s._source.isBeingEdited())
|
||||
console.log('state', s._source.state())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -649,3 +651,17 @@ export class DLine extends Line {
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
export class DateRange extends Shape {
|
||||
constructor(model, onModel=null, onDelete=null, props=null) {
|
||||
super(ShapeType.DateRange, onModel, onDelete, props)
|
||||
}
|
||||
|
||||
|
||||
setModel(model) {
|
||||
super.setModel(model);
|
||||
if (model.startTime !== this.model.startTime || model.endTime !== this.model.endTime)
|
||||
this.setPoints([{time: model.startTime}, {time: model.endTime}])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export const NATIVE_TOKEN = '0x0000000000000000000000000000000000000001'
|
||||
export const USD_FIAT = '0x0000000000000000000000000000000000000055' // We use 0x55 (ASCII 'U') to indicate the use of fiat USD
|
||||
|
||||
export function mixin(child, ...parents) {
|
||||
// child is modified directly, assigning fields from parents that are missing in child. parents fields are
|
||||
// assigned by parents order, highest priority first
|
||||
@@ -151,3 +154,37 @@ export function timestamp(date = null) {
|
||||
export function dateString(datetime) {
|
||||
return datetime.toLocaleString({dateStyle: 'medium', timeStyle: 'short'})
|
||||
}
|
||||
|
||||
export function logicalXOR(a, b) {
|
||||
return (a || b) && !(a && b)
|
||||
}
|
||||
|
||||
// Base62
|
||||
|
||||
// base62.js
|
||||
const base62charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
export function encodeBase62(num) {
|
||||
if (num === 0) return base62charset[0];
|
||||
let encoded = "";
|
||||
while (num > 0) {
|
||||
encoded = base62charset[num % 62] + encoded;
|
||||
num = Math.floor(num / 62);
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
export function decodeBase62(str) {
|
||||
return str.split('').reverse().reduce((acc, char, i) =>
|
||||
acc + base62charset.indexOf(char) * Math.pow(62, i), 0);
|
||||
}
|
||||
|
||||
|
||||
export function withTimeout(promise, timeoutDuration, fallbackErrorMessage) {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(fallbackErrorMessage)), timeoutDuration)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import {usePrefStore, useStore} from "@/store/store";
|
||||
import {computed} from "vue";
|
||||
import {DateTime, Info} from "luxon";
|
||||
|
||||
const s = useStore()
|
||||
const prefs = usePrefStore()
|
||||
const props = defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const hideDetails = true
|
||||
|
||||
const now = computed(()=>DateTime.fromSeconds(props.modelValue?props.modelValue:0).setZone(s.timeZone))
|
||||
const now = computed(()=>DateTime.fromSeconds(props.modelValue?props.modelValue:0).setZone(prefs.timezone))
|
||||
|
||||
const year = computed({
|
||||
get() { return now.value.year },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<v-alert class="d-sm-none" type="info" title="Mobile Screen" text="Dexorder is designed for desktop. Mobile coming soon!" rounded="0"/>
|
||||
<v-alert v-if='!s.connected' icon="mdi-wifi-off" type="error" title="Not Connected" class="mb-3" rounded="0" density="compact"/>
|
||||
<v-alert v-for="e in s.errors" icon="mdi-alert" type="error" :title="e.title" :text="e.text" class="mb-3" :closable="e.closeable" rounded="0"/>
|
||||
</template>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import {socket} from "@/socket.js";
|
||||
import {useRoute} from "vue-router";
|
||||
import {socket} from "@/socket.js";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<!--
|
||||
<v-chip text="BETA" size='x-small' color="red" class="align-self-start" variant="text"/>
|
||||
-->
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<v-tooltip :model-value="!error&&copied" :open-on-hover="false" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<span v-bind="props" style="cursor: pointer" @click="copy()">
|
||||
<div class="d-inline-flex align-center" v-bind="props" style="cursor: pointer" @click="copy()">
|
||||
<slot>
|
||||
<span :style="{maxWidth:width}" style="display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{ text }}</span>
|
||||
</slot>
|
||||
<v-btn v-bind="props" v-if="permitted" rounded variant="text" size="small" density="compact" @click="copy()"
|
||||
:class="error?'error':copied?'success':''"
|
||||
:icon="error?'mdi-close-box-outline':copied?'mdi-check-circle-outline':'mdi-content-copy'"
|
||||
:text="showText?text:''"
|
||||
/>
|
||||
<slot>{{text}}</slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<span>Copied!</span>
|
||||
</v-tooltip>
|
||||
@@ -17,7 +19,7 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
|
||||
const props = defineProps({text:String, showText:{default:false}})
|
||||
const props = defineProps({text:String, showText:{default:false}, width: {default:null}})
|
||||
const permitted = ref(true)
|
||||
const copied = ref(false)
|
||||
const error = ref(false)
|
||||
|
||||
65
src/components/DebugConsole.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="debug-console">
|
||||
<div v-for="(entry, idx) in logs" :key="idx" :class="['log-entry', entry.type]">
|
||||
[{{ entry.type }}] {{ entry.message }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const logs = ref([]);
|
||||
|
||||
function stringify(args) {
|
||||
return args.map(arg => {
|
||||
if (typeof arg === 'object') {
|
||||
try { return JSON.stringify(arg); } catch { return String(arg); }
|
||||
}
|
||||
return String(arg);
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
let original = {};
|
||||
|
||||
onMounted(() => {
|
||||
['log', 'warn', 'error', 'info'].forEach(type => {
|
||||
// Save the original method
|
||||
original[type] = console[type];
|
||||
console[type] = function(...args) {
|
||||
logs.value.push({
|
||||
type,
|
||||
message: stringify(args)
|
||||
});
|
||||
// Optionally limit log length:
|
||||
if (logs.value.length > 200) logs.value.shift();
|
||||
original[type].apply(console, args);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// Restore the original methods
|
||||
Object.keys(original).forEach(type => {
|
||||
if (original[type]) console[type] = original[type];
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.debug-console {
|
||||
background: #1a1a1a;
|
||||
color: #0f0;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
border-radius: 5px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.log-entry { margin-bottom: 2px; }
|
||||
.log-entry.warn { color: #ff0; }
|
||||
.log-entry.error { color: #f55; }
|
||||
.log-entry.info { color: #0cf; }
|
||||
</style>
|
||||
@@ -36,8 +36,8 @@
|
||||
import {useStore} from "@/store/store";
|
||||
import {computed, ref} from "vue";
|
||||
import Btn from "@/components/Btn.vue";
|
||||
import {socket} from "@/socket.js";
|
||||
import {metadata} from "@/version.js";
|
||||
import {socket} from "@/socket.js";
|
||||
|
||||
const DISABLED_DURATION = 60_000;
|
||||
|
||||
|
||||
142
src/components/FloatingDiv.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div
|
||||
ref="floatingDiv"
|
||||
:style="divStyle"
|
||||
@mousedown="startDrag"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
}
|
||||
});
|
||||
|
||||
const floatingDiv = ref(null);
|
||||
const position = reactive({ x: 0, y: 0 });
|
||||
const isDragging = ref(false);
|
||||
const isFloating = ref(false);
|
||||
const dragStartOffset = reactive({ x: 0, y: 0 });
|
||||
|
||||
const divStyle = reactive({
|
||||
position: "",
|
||||
top: "",
|
||||
left: "",
|
||||
zIndex: "",
|
||||
cursor: "default",
|
||||
});
|
||||
|
||||
let activeComponent = null;
|
||||
|
||||
function positionKey() {
|
||||
return props.id ? `floating-div-pos:${props.id}` : null;
|
||||
}
|
||||
|
||||
function savePosition() {
|
||||
if (props.id) {
|
||||
localStorage.setItem(
|
||||
positionKey(),
|
||||
JSON.stringify({ x: position.x, y: position.y })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function loadPosition() {
|
||||
if (props.id) {
|
||||
const data = localStorage.getItem(positionKey());
|
||||
if (data) {
|
||||
try {
|
||||
const { x, y } = JSON.parse(data);
|
||||
position.x = x;
|
||||
position.y = y;
|
||||
divStyle.position = "fixed";
|
||||
divStyle.top = `${position.y}px`;
|
||||
divStyle.left = `${position.x}px`;
|
||||
divStyle.zIndex = 9999;
|
||||
isFloating.value = true;
|
||||
} catch {
|
||||
// Ignore corrupted data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startDrag = (event) => {
|
||||
if (event.shiftKey || event.ctrlKey) {
|
||||
if (!isFloating.value) {
|
||||
const rect = floatingDiv.value.getBoundingClientRect();
|
||||
position.x = rect.left;
|
||||
position.y = rect.top;
|
||||
divStyle.position = "fixed";
|
||||
divStyle.top = `${position.y}px`;
|
||||
divStyle.left = `${position.x}px`;
|
||||
divStyle.zIndex = 9999;
|
||||
isFloating.value = true;
|
||||
}
|
||||
|
||||
isDragging.value = true;
|
||||
dragStartOffset.x = event.clientX - position.x;
|
||||
dragStartOffset.y = event.clientY - position.y;
|
||||
divStyle.cursor = "move";
|
||||
document.body.style.userSelect = "none";
|
||||
|
||||
activeComponent = floatingDiv;
|
||||
|
||||
window.addEventListener("mousemove", handleDrag);
|
||||
window.addEventListener("mouseup", stopDrag);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrag = (event) => {
|
||||
if (isDragging.value && activeComponent === floatingDiv) {
|
||||
position.x = event.clientX - dragStartOffset.x;
|
||||
position.y = event.clientY - dragStartOffset.y;
|
||||
divStyle.top = `${position.y}px`;
|
||||
divStyle.left = `${position.x}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
if (isDragging.value && activeComponent === floatingDiv) {
|
||||
isDragging.value = false;
|
||||
divStyle.cursor = "default";
|
||||
document.body.style.userSelect = "auto";
|
||||
activeComponent = null;
|
||||
|
||||
savePosition();
|
||||
|
||||
window.removeEventListener("mousemove", handleDrag);
|
||||
window.removeEventListener("mouseup", stopDrag);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOutsideDrag = (event) => {
|
||||
if (event.key === "Shift" || event.key === "Control") {
|
||||
stopDrag();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("keyup", handleOutsideDrag);
|
||||
loadPosition();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("keyup", handleOutsideDrag);
|
||||
window.removeEventListener("mousemove", handleDrag);
|
||||
window.removeEventListener("mouseup", stopDrag);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div[ref="floatingDiv"] {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
v-model="os.tranches" :rules="[validateRequired,validateTranches]">
|
||||
<template v-slot:append-inner>tranches</template>
|
||||
</v-text-field>
|
||||
<v-text-field label='Skew' type="number" step="10" aria-valuemin="0" min="-100" max="100" variant="outlined"
|
||||
<v-text-field label='Balance' type="number" step="10" aria-valuemin="0" min="-100" max="100" variant="outlined"
|
||||
v-model="skew" clearable @click:clear="skew=0" suffix="%"/>
|
||||
<!-- todo deadline -->
|
||||
<v-table>
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
<template>
|
||||
<div class="d-inline-flex">
|
||||
<v-img :src="`dexorder_full_${s.theme}mode.svg`" width="6em" inline/>
|
||||
<beta/>
|
||||
<v-img v-if="variant==='full'" :src="`/logo/dexorder_full_${s.theme}mode.svg`" width="6em" inline/>
|
||||
<v-img v-if="variant==='icon'" :src="`/logo/ico_${s.theme==='dark'?'black':'white'}_clip.png`" :width="width" inline/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import Beta from "@/components/Beta.vue";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {computed} from "vue";
|
||||
|
||||
const s = useStore()
|
||||
const props = defineProps({
|
||||
showTag: {type: Boolean, default: false}
|
||||
variant: {type: String, default: 'full', /* 'icon', */ },
|
||||
size: {type: String, default: 'medium'},
|
||||
})
|
||||
|
||||
const width = computed(()=>{
|
||||
return {
|
||||
'x-small': '0.75em',
|
||||
'small': '1.0em',
|
||||
'medium': '1.25em',
|
||||
'large': '1.75em',
|
||||
'x-large': '2.5em',
|
||||
}[props.size] || props.size;
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import router from "@/router/index.js";
|
||||
import {router} from "@/router/router.js";
|
||||
const s = useStore()
|
||||
|
||||
function nav(path) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<v-card v-if="status!==Status.OK" rounded="0">
|
||||
<v-card-title>
|
||||
<!-- <v-icon icon="mdi-hand-wave" color="grey"/>-->
|
||||
Welcome to Dexorder Beta!
|
||||
Welcome to Dexorder!
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
Play with the order builder without an account by clicking on the <logo class="logo-small"/> logo or on
|
||||
@@ -40,11 +40,12 @@
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import {computed, ref} from "vue";
|
||||
import {addNetwork, connectWallet, switchChain} from "@/blockchain/wallet.js";
|
||||
import {addNetworkAndConnectWallet} from "@/blockchain/wallet.js";
|
||||
import Btn from "@/components/Btn.vue";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
import ApproveRegion from "@/components/ApproveRegion.vue";
|
||||
import TermsOfService from "@/components/TermsOfService.vue";
|
||||
import {track} from "@/track.js";
|
||||
|
||||
const s = useStore()
|
||||
const disabled = ref(false)
|
||||
@@ -69,32 +70,7 @@ function reload() {
|
||||
async function connect() {
|
||||
disabled.value = true
|
||||
try {
|
||||
try {
|
||||
await switchChain(s.chainId)
|
||||
}
|
||||
catch (e) {
|
||||
if (e.code===4001) {
|
||||
// explicit user rejection
|
||||
return
|
||||
}
|
||||
else if (e.code===4902) {
|
||||
try {
|
||||
await addNetwork(s.chainId)
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`Could not add network ${s.chainId}`)
|
||||
}
|
||||
}
|
||||
else
|
||||
console.log('switchChain() failure',e)
|
||||
}
|
||||
try {
|
||||
await connectWallet(s.chainId)
|
||||
}
|
||||
catch (e) {
|
||||
if (e.code!==4001)
|
||||
console.log('connectWallet() failed', e)
|
||||
}
|
||||
await addNetworkAndConnectWallet(s.chainId);
|
||||
}
|
||||
finally {
|
||||
disabled.value = false
|
||||
|
||||
40
src/components/OneTimeHint.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<v-tooltip v-model="show" :close-on-content-click="true" :close-on-back="false" :close-delay="null"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import {usePrefStore} from "@/store/store.js";
|
||||
|
||||
const prefs = usePrefStore()
|
||||
|
||||
const props = defineProps({
|
||||
name: {type: String, required: true},
|
||||
when: {type: Boolean, default: true}, // optional conditional for when to show
|
||||
after: {type: String, default: null}, // set to the name of another hint that must happen before this hint, to chain hints into a tutorial.
|
||||
onComplete: {type: Function, default: null},
|
||||
})
|
||||
|
||||
const forceClose = ref(false)
|
||||
const shown = ref(false)
|
||||
|
||||
const show = computed({
|
||||
get() {
|
||||
const shownBefore = prefs.hints[props.name];
|
||||
const whenOk = props.when;
|
||||
const afterOk = props.after === null || prefs.hints[props.after];
|
||||
const result = !forceClose.value && !shownBefore && whenOk && afterOk
|
||||
// console.log(`show ${props.name}? ${result} <=`, !forceClose.value, whenOk, afterOk, prefs.hints)
|
||||
if (result) {
|
||||
shown.value = true
|
||||
prefs.hints[props.name] = true
|
||||
}
|
||||
return result
|
||||
},
|
||||
set(v) { if(!v) { forceClose.value=true; if (shown.value && props.onComplete) props.onComplete(); } }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -11,7 +11,8 @@
|
||||
</div>
|
||||
</v-card-item>
|
||||
<v-card-actions class="d-flex justify-space-evenly mb-4">
|
||||
<v-btn variant="outlined" color="red" @click="nav('Assets')">Cancel</v-btn>
|
||||
<v-btn variant="outlined" color="red" @click="// noinspection JSIgnoredPromiseFromCall
|
||||
router.push({name: 'Assets'})">Cancel</v-btn>
|
||||
<v-btn variant="flat" color="green" :disabled="!valid()" @click="placeOrder">Place Dexorder</v-btn>
|
||||
</v-card-actions>
|
||||
</phone-card>
|
||||
@@ -23,12 +24,13 @@ import {useOrderStore, useStore} from "@/store/store";
|
||||
import PhoneCard from "@/components/PhoneCard.vue";
|
||||
import Amount from "@/components/Amount.vue"
|
||||
// noinspection ES6UnusedImports
|
||||
import {nav, SingletonCoroutine, vAutoSelect} from "@/misc.js";
|
||||
import {SingletonCoroutine, vAutoSelect} from "@/misc.js";
|
||||
import {newOrder} from "@/blockchain/orderlib.js";
|
||||
import {FixedNumber} from "ethers";
|
||||
import PairChoice from "@/components/PairChoice.vue";
|
||||
import NeedsSigner from "@/components/NeedsSigner.vue";
|
||||
import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import {router} from "@/router/router.js";
|
||||
|
||||
const s = useStore()
|
||||
const os = useOrderStore()
|
||||
|
||||
84
src/components/PoolSelectionDialog.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<v-dialog v-model="co.showPoolSelection" max-width="300">
|
||||
<v-card title="Pool Selection">
|
||||
<v-card-text>
|
||||
<div class="mb-2">
|
||||
<v-avatar image="/arbitrum-logo.svg" size="x-small" class="mr-1"/> Arbitrum One
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<v-avatar image="/uniswap-logo.svg" size="x-small" class="mr-1" style="background-color: white"/> Uniswap v3
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>Pool</td><td><scanner-button :addr="co.selectedSymbol.address"/></td></tr>
|
||||
<tr><td>{{ co.selectedSymbol.base.s }}</td><td><scanner-button :addr="co.selectedSymbol.base.a"/></td></tr>
|
||||
<tr><td>{{ co.selectedSymbol.quote.s }}</td><td><scanner-button :addr="co.selectedSymbol.quote.a"/></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<v-table class="mt-4">
|
||||
<thead>
|
||||
<tr><th>Selected</th><th>Fee</th><td>Liquidity</td></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="([addr,fee],i) in co.selectedSymbol.feeGroup"
|
||||
:class="co.selectedSymbol.fee === fee ? 'selected' : 'selectable'" @click="selectFee(fee)">
|
||||
<td><v-icon v-if="co.selectedSymbol.fee===fee" icon="mdi-check"/></td>
|
||||
<td>{{(fee/10_000).toFixed(2)}}%</td>
|
||||
<td>{{ !co.selectedSymbol.liquiditySymbol ? '' :
|
||||
co.selectedSymbol.liquiditySymbol === 'USD' ?
|
||||
`$${toHuman(co.selectedSymbol.liquidities[i])}` :
|
||||
toHuman(co.selectedSymbol.liquidities[i]) +' '+co.selectedSymbol.liquiditySymbol }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {toHuman} from "@/misc.js";
|
||||
import ScannerButton from "@/components/ScannerButton.vue";
|
||||
import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import {feelessTickerKey} from "@/charts/datafeed.js";
|
||||
import {setSymbolTicker} from "@/charts/chart.js";
|
||||
|
||||
const co = useChartOrderStore()
|
||||
|
||||
|
||||
function selectFee(fee) {
|
||||
if (fee === co.selectedSymbol.fee) return
|
||||
const symbol = co.selectedSymbol;
|
||||
const ticker = feelessTickerKey(symbol.ticker) + '|' + fee
|
||||
if (ticker !== symbol.ticker)
|
||||
setSymbolTicker(ticker).catch((e) => console.error('Could not change TV symbol to', ticker))
|
||||
co.showPoolSelection = false
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
|
||||
td {
|
||||
padding-right: 1em;
|
||||
}
|
||||
tr.selected {
|
||||
|
||||
}
|
||||
tr.selectable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.pool-button {
|
||||
color: blue;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
34
src/components/ScannerButton.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="d-inline-flex align-center" style="cursor: pointer">
|
||||
<span @click='click'
|
||||
:style="{maxWidth: width, width}" style="white-space: nowrap; display: inline-block; overflow: hidden; text-overflow: ellipsis">{{addr}}</span>
|
||||
<v-icon icon="mdi-open-in-new" size="x-small"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import {useStore} from "@/store/store.js";
|
||||
|
||||
const props = defineProps({chainId: {type: Number, default:null}, addr:String, width: {default:'5em'}})
|
||||
const s = useStore()
|
||||
|
||||
function click() {
|
||||
window.open(url.value, '_blank')
|
||||
}
|
||||
|
||||
const url = computed(()=>{
|
||||
const chain = props.chainId ? props.chainId: s.chainId
|
||||
switch (chain) {
|
||||
case 31337:
|
||||
case 1337:
|
||||
case 42161:
|
||||
return `https://arbiscan.io/address/${props.addr}`
|
||||
}
|
||||
throw new Error(`No scanner defined for chain ${chain}`)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-data-table :headers="datatableHeaders" :items="orders" item-value="id"
|
||||
:show-select="true" :show-expand="true" v-model="selected" select-strategy="single">
|
||||
:show-select="false" :show-expand="true" v-model="selected" select-strategy="single"
|
||||
:row-props="rowPropsHandler" :hover="true"
|
||||
>
|
||||
<template v-slot:item.startTime="{ value }">{{ timestampString(value) }}</template>
|
||||
<template v-slot:item.input="{ item }">
|
||||
<span v-if="item.order.amountIsInput">
|
||||
@@ -71,12 +73,6 @@
|
||||
</suspense>
|
||||
</template>
|
||||
<template v-slot:item.state="{ item }">
|
||||
<!--
|
||||
<v-chip v-if="item.state===PendingOrderState.Submitted || item.state===PendingOrderState.Signing"
|
||||
prepend-icon="mdi-signature">Wallet Signing</v-chip>
|
||||
<v-chip v-if="item.state===PendingOrderState.Rejected" prepend-icon="mdi-cancel">Rejected</v-chip>
|
||||
<v-chip v-if="item.state===PendingOrderState.Sent" prepend-icon="mdi-send">Sent</v-chip>
|
||||
-->
|
||||
<v-chip v-if="item.state===OrderState.Open" class="d-none d-lg-inline-flex" prepend-icon="mdi-dots-horizontal"
|
||||
color="green">Open
|
||||
</v-chip>
|
||||
@@ -90,28 +86,18 @@
|
||||
</v-chip>
|
||||
<v-chip v-if="item.state===OrderState.Expired" prepend-icon="mdi-progress-check" color="grey-darken-1">Partial
|
||||
</v-chip>
|
||||
<v-chip v-if="item.state===OrderState.Underfunded" prepend-icon="mdi-alert" color="warning">Underfunded</v-chip>
|
||||
<v-chip v-if="item.state===OrderState.Underfunded" prepend-icon="mdi-alert" color="warning">Underfunded
|
||||
<v-tooltip :text="`This order is underfunded. Add more ${item.inSymbol} to your vault.`" location="top" activator="parent"></v-tooltip>
|
||||
</v-chip>
|
||||
<v-chip v-if="item.state===OrderState.Error" prepend-icon="mdi-alert" color="error">Error</v-chip>
|
||||
</template>
|
||||
<!-- <template v-slot:item.action="{item}">-->
|
||||
<!-- <btn v-if="item.state===OrderState.Open" icon="mdi-cancel" color="red"-->
|
||||
<!-- @click="cancelOrder(vaultAddr,item.index)">Cancel-->
|
||||
<!-- </btn>-->
|
||||
<!-- </template>-->
|
||||
<template v-slot:expanded-row="{item}">
|
||||
<template v-for="(t, i) in item.order.tranches">
|
||||
<tr>
|
||||
<td class="text-right" colspan="2">
|
||||
<div class="text-right">
|
||||
<div v-if="s.clock < item.trancheStatus[i].startTime">
|
||||
Activates {{ timestampString(item.trancheStatus[i].startTime) }}
|
||||
</div>
|
||||
<div v-if="s.clock < item.trancheStatus[i].endTime && item.trancheStatus[i].endTime < DISTANT_FUTURE">
|
||||
Expires {{ timestampString(item.trancheStatus[i].endTime) }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mx-3" v-if="t.marketOrder">market order</div>
|
||||
<div class="mx-3" v-if="t.marketOrder && t.rateLimitPeriod === 0">market order</div>
|
||||
<div class="mx-3" v-if="t.marketOrder && t.rateLimitPeriod !== 0">DCA {{Math.round(MAX_FRACTION/t.rateLimitFraction)}} parts</div>
|
||||
<line-price class="mx-3" v-if="!t.marketOrder"
|
||||
:base="item.base" :quote="item.quote"
|
||||
:b="t.minLine.intercept" :m="t.minLine.slope" :is-breakout="!item.minIsLimit"
|
||||
@@ -121,7 +107,10 @@
|
||||
:b="t.maxLine.intercept" :m="t.maxLine.slope" :is-breakout="item.minIsLimit"
|
||||
:show-btn="true"/>
|
||||
</div>
|
||||
|
||||
<div class="text-right" v-if="item.state===OrderState.Open">
|
||||
<div>{{ describeTrancheTime(item, i, true) }}</div>
|
||||
<div>{{ describeTrancheTime(item, i, false) }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<suspense>
|
||||
@@ -176,17 +165,18 @@
|
||||
<script setup>
|
||||
import LinePrice from "@/components/LinePrice.vue";
|
||||
import {useStore} from "@/store/store";
|
||||
import {computed, defineAsyncComponent, onUnmounted, ref, watch} from "vue";
|
||||
import {computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch} from "vue";
|
||||
import Btn from "@/components/Btn.vue"
|
||||
import {cancelOrder, useWalletStore} from "@/blockchain/wallet.js";
|
||||
import {DISTANT_FUTURE, isOpen, OrderState} from "@/blockchain/orderlib.js";
|
||||
import {DISTANT_FUTURE, isOpen, MAX_FRACTION, OrderState} from "@/blockchain/orderlib.js";
|
||||
import Pulse from "@/components/Pulse.vue";
|
||||
import {OrderShapes} from "@/charts/ordershapes.js";
|
||||
import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import {lookupSymbol, tickerForOrder} from "@/charts/datafeed.js";
|
||||
import {setSymbol} from "@/charts/chart.js";
|
||||
import {addSymbolChangedCallback, removeSymbolChangedCallback, setSymbol} from "@/charts/chart.js";
|
||||
import {uniswapV3AveragePrice} from "@/blockchain/uniswap.js";
|
||||
import {timestampString} from "@/misc.js";
|
||||
import {metadataMap} from "@/version.js";
|
||||
|
||||
const PairPrice = defineAsyncComponent(()=>import("@/components/PairPrice.vue"))
|
||||
const TokenAmount = defineAsyncComponent(()=>import('./TokenAmount.vue'))
|
||||
@@ -250,6 +240,16 @@ const datatableHeaders = [
|
||||
// {title: '', align: 'end', key: 'action'},
|
||||
];
|
||||
|
||||
const rowPropsHandler = ({ item }) => ({
|
||||
class: selected.value.indexOf(item.id) !== -1 ? 'selected-row' : '',
|
||||
onClick: () => {
|
||||
if (selected.value.indexOf(item.id) === -1)
|
||||
selected.value = [item.id]
|
||||
else
|
||||
selected.value = []
|
||||
},
|
||||
})
|
||||
|
||||
const orders = computed(()=>{
|
||||
// example twap
|
||||
// status = [
|
||||
@@ -318,7 +318,6 @@ const orders = computed(()=>{
|
||||
st.id = `${vault}|${index}`
|
||||
st.index = parseInt(index)
|
||||
// st.startTime = timestampString(st.startTime)
|
||||
console.log('starttime', st.startTime)
|
||||
/*
|
||||
o.tranches = o.tranches.map((tranche)=>{
|
||||
const t = {...tranche}
|
||||
@@ -356,19 +355,21 @@ const orders = computed(()=>{
|
||||
for (const st of result) {
|
||||
let low, high;
|
||||
// console.log('elab', st.order)
|
||||
const buy = st.order.tokenIn > st.order.tokenOut;
|
||||
const o = st.order;
|
||||
const buy = o.tokenIn > o.tokenOut;
|
||||
if (buy) {
|
||||
low = st.order.tokenOut
|
||||
high = st.order.tokenIn
|
||||
low = o.tokenOut
|
||||
high = o.tokenIn
|
||||
}
|
||||
else {
|
||||
low = st.order.tokenIn
|
||||
high = st.order.tokenOut
|
||||
low = o.tokenIn
|
||||
high = o.tokenOut
|
||||
}
|
||||
st.base = st.order.inverted ? high : low;
|
||||
st.quote = st.order.inverted ? low : high;
|
||||
st.minIsLimit = buy === st.order.inverted // whether limit/breakout is flipped
|
||||
// console.log('buy/inverted/minIsLimit', buy, st.order.inverted, st.minIsLimit)
|
||||
st.base = o.inverted ? high : low;
|
||||
st.quote = o.inverted ? low : high;
|
||||
st.minIsLimit = buy === o.inverted // whether limit/breakout is flipped
|
||||
const found = metadataMap[st.chainId][o.tokenIn]
|
||||
st.inSymbol = found ? found.s : o.tokenIn
|
||||
// console.log('elaborated', st)
|
||||
}
|
||||
result.sort((a,b)=>b.startTime-a.startTime)
|
||||
@@ -383,7 +384,6 @@ const orders = computed(()=>{
|
||||
function describeTrancheTime(st, trancheIndex, isStart) {
|
||||
const t = st.order.tranches[trancheIndex]
|
||||
const ts = st.trancheStatus[trancheIndex]
|
||||
console.log('describeTT', t, ts)
|
||||
let result = ''
|
||||
if( t.minIsBarrier || t.maxIsBarrier )
|
||||
return 'barrier'
|
||||
@@ -401,12 +401,16 @@ function describeTrancheTime(st, trancheIndex, isStart) {
|
||||
return result
|
||||
}
|
||||
|
||||
function symbolChanged() {
|
||||
selected.value = []
|
||||
}
|
||||
|
||||
onMounted(()=>addSymbolChangedCallback(symbolChanged))
|
||||
onUnmounted(()=>removeSymbolChangedCallback(symbolChanged))
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
|
||||
// columns
|
||||
.num {
|
||||
min-width: 1em;
|
||||
@@ -429,5 +433,12 @@ function describeTrancheTime(st, trancheIndex, isStart) {
|
||||
}
|
||||
.cancel {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@use "src/styles/settings" as *;
|
||||
|
||||
tr.selected-row {
|
||||
background-color: #616161;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
<script setup>
|
||||
if(window.location.hostname !== 'localhost') {
|
||||
window.$crisp = [];window.CRISP_WEBSITE_ID = "b153f30a-4b0b-49cc-8a38-989409a73acb";(function () {const d = document;const s = d.createElement("script");s.src = "https://client.crisp.chat/l.js";s.async = 1;d.getElementsByTagName("head")[0].appendChild(s);})();
|
||||
window.$crisp=[];window.CRISP_WEBSITE_ID="a88f707f-adee-4960-b6de-c328c9d86945";(function(){const d=document;const s=d.createElement("script");s.src="https://client.crisp.chat/l.js";s.async=1;d.getElementsByTagName("head")[0].appendChild(s);})();
|
||||
}
|
||||
</script>
|
||||
@@ -22,8 +22,8 @@
|
||||
import {usePrefStore} from "@/store/store.js";
|
||||
import {computed, ref, watch} from "vue";
|
||||
|
||||
import {socket} from "@/socket.js";
|
||||
import TosCard from "@/components/TosCard.vue";
|
||||
import {socket} from "@/socket.js";
|
||||
|
||||
// UPDATE THIS WHEN CHANGING THE TOS
|
||||
const CURRENT_VERSION='2024-11-18' // this must be a parseable date
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<v-card-text class="text-center">Last Updated November 18, 2024</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
Please read these Terms of Service (the “<b>Terms</b>”) and our <a href="https://dexorder.trade/privacy-policy" target="dexorder">Privacy Policy</a> carefully because they govern your
|
||||
Please read these Terms of Service (the “<b>Terms</b>”) and our <a href="https://dexorder.com/privacy-policy" target="dexorder">Privacy Policy</a> carefully because they govern your
|
||||
use of the
|
||||
website (and all subdomains and subpages thereon) located at dexorder.trade, including without limitation the
|
||||
subdomains app.dexorder.trade and www.dexorder.trade (collectively, the “<b>Site</b>”), and the Dexorder web
|
||||
website (and all subdomains and subpages thereon) located at dexorder.com, including without limitation the
|
||||
subdomains app.dexorder.com and www.dexorder.com (collectively, the “<b>Site</b>”), and the Dexorder web
|
||||
application graphical user interface and any other services accessible via the Site (together with the Site, web
|
||||
application, and other services, collectively, the “<b>Dexorder Service</b>”) offered by Dexorder Trading Services
|
||||
Ltd.
|
||||
@@ -205,7 +205,7 @@
|
||||
(i) Subject to your compliance with these Terms, Dexorder will use its commercially
|
||||
reasonable efforts to provide you with access to the Dexorder Service and to cause your Interactions to be
|
||||
executed on the applicable DEX in accordance with Dexorder’s Execution Policy located at
|
||||
<a href="https://dexorder.trade/execution-policy">https://dexorder.trade/execution-policy</a>
|
||||
<a href="https://dexorder.com/execution-policy">https://dexorder.com/execution-policy</a>
|
||||
(“<b>Execution Policy</b>”), however from time to time the Site and the Dexorder Service may be inaccessible or
|
||||
inoperable for any
|
||||
reason, including, without limitation: (a) if an Interaction repeatedly fails to be executed (such as due to an
|
||||
@@ -690,7 +690,7 @@
|
||||
<v-card-title>18. Contact Information</v-card-title>
|
||||
<v-card-text>
|
||||
If you have any questions about these Terms or the Dexorder Service, please contact Dexorder
|
||||
at <a href="mailto:legal@dexorder.trade">legal@dexorder.trade</a> or <a href="mailto:support@dexorder.trade">support@dexorder.trade</a>.
|
||||
at <a href="mailto:legal@dexorder.com">legal@dexorder.com</a> or <a href="mailto:support@dexorder.com">support@dexorder.com</a>.
|
||||
</v-card-text>
|
||||
|
||||
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
<v-card-item v-if="!empty">
|
||||
<v-table>
|
||||
<tbody>
|
||||
<native-row v-if="nativeBalance" :chain-id="s.chainId" :addr="s.vault" :amount="nativeBalance"
|
||||
<native-row v-if="nativeBalance && BigInt(nativeBalance)>0n" :chain-id="s.chainId" :addr="s.vault" :amount="nativeBalance"
|
||||
:on-withdraw="onWithdrawNative" :on-wrap="()=>wrapShow=true"/>
|
||||
<suspense v-for="(amount,addr) of balances">
|
||||
<token-row v-if="BigInt(amount)!==0n" :chain-id="s.chainId" :addr="addr" :amount="amount" :onWithdraw="onWithdraw"/>
|
||||
<token-row v-if="amount && BigInt(amount)>0n" :chain-id="s.chainId" :addr="addr" :amount="amount" :onWithdraw="onWithdraw"/>
|
||||
</suspense>
|
||||
</tbody>
|
||||
</v-table>
|
||||
@@ -60,12 +60,13 @@
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {computed, defineAsyncComponent, onUnmounted, ref, watchEffect} from "vue";
|
||||
import {vaultAddress} from "@/blockchain/contract.js";
|
||||
import {ensureVault, provider} from "@/blockchain/wallet.js";
|
||||
import {ensureVault} from "@/blockchain/wallet.js";
|
||||
import CopyButton from "@/components/CopyButton.vue";
|
||||
import Withdraw from "@/components/Withdraw.vue";
|
||||
import NativeRow from "@/components/NativeRow.vue";
|
||||
import NativeWrap from "@/components/NativeWrap.vue";
|
||||
import WithdrawNative from "@/components/WithdrawNative.vue";
|
||||
import {provider} from "@/blockchain/provider.js";
|
||||
|
||||
const TokenRow = defineAsyncComponent(()=>import('./TokenRow.vue'))
|
||||
const s = useStore()
|
||||
@@ -89,7 +90,7 @@ const hasVault = computed(()=>s.vault!==null)
|
||||
const withdrawToken = ref(null)
|
||||
const withdrawShow = ref(false)
|
||||
async function onWithdraw(token) {
|
||||
console.log('withdraw', addr, token)
|
||||
console.log('withdraw', addr.value, token)
|
||||
withdrawToken.value = token
|
||||
withdrawShow.value = true
|
||||
}
|
||||
|
||||
72
src/components/WelcomeDialog.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<v-dialog v-model="modelValue" max-width="600">
|
||||
<v-card title="Welcome to Dexorder!">
|
||||
<template #prepend><logo variant="icon" size="large"/></template>
|
||||
<v-card-text>
|
||||
<div class="d-flex">
|
||||
Dexorder powers up <div class="d-inline-flex align-self-baseline"><v-img src="/uniswap-logo.svg" width="1.25em" inline/></div>Uniswap with advanced ordering capabilities.
|
||||
</div>
|
||||
<p class="mt-2">
|
||||
Dexorder gives you a personal trading vault smart contract that trades its tokens with Uniswap only when
|
||||
your advanced order conditions are met.
|
||||
</p>
|
||||
<p class="mt-2">Level up your DeFi trading with:</p>
|
||||
<v-list density="compact">
|
||||
<v-list-item prepend-icon="mdi-clock-outline"><b>DCA / TWAP</b> <small>up to 1000 parts</small></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-ray-vertex"><b>Limit Ladders</b> <small>capture ranges</small></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-vector-line"><b>Diagonal Limits</b> <small>trade trends and channels</small></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-chart-line"><b>Breakout Orders</b> <small>buy <i>above</i> a price level</small></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-plus-minus"><b>Stop-loss</b> <small>coming soon</small></v-list-item>
|
||||
<!-- <v-list-item prepend-icon="mdi-cancel">One-click Cancel All</v-list-item>-->
|
||||
<!--
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-avatar image="/arbitrum-logo.svg" size="1.5em" class="mr-4"/>
|
||||
</template>
|
||||
<template #default>
|
||||
<b>Arbitrum One</b> support <small>fast and cheap</small>
|
||||
</template>
|
||||
</v-list-item>
|
||||
-->
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-avatar image="/uniswap-logo.svg" size="1.5em" class="mr-4" style="background-color: white"/>
|
||||
</template>
|
||||
<template #default>
|
||||
<b>Uniswap</b> v3 support <small>high liquidity</small>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center mb-9">
|
||||
<!-- <v-btn variant="text" @click="learnMore" class="justify-end">Learn More</v-btn>-->
|
||||
<v-btn variant="tonal" color="primary" @click="tryIt" class="justify-center">Try It!</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {track} from "@/track.js";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
|
||||
const modelValue = defineModel()
|
||||
|
||||
function tryIt() {
|
||||
track('tutorial_begin')
|
||||
modelValue.value = false
|
||||
}
|
||||
|
||||
function learnMore() {
|
||||
track('learn-more')
|
||||
window.open('https://dexorder.com/introduction.html', 'dexorderwww')
|
||||
modelValue.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
small {
|
||||
margin-left: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
<v-btn variant="text" text="max" @click="floatAmount=balanceFloat"/>
|
||||
</template>
|
||||
<template v-slot:append-inner>
|
||||
<span>{{ token.s }}</span>
|
||||
<span style="max-width: 6em">{{ token.s }}</span>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-card-actions>
|
||||
@@ -36,11 +36,17 @@ const s = useStore()
|
||||
const props = defineProps(['modelValue', 'vault', 'token'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const balance = computed(() => {
|
||||
console.log('balance', props.vault, props.token, s.vaultBalances)
|
||||
const b = s.vaultBalances[props.vault][props.token.address];
|
||||
const b = s.getBalance(props.token?.a)
|
||||
console.log('balance', b)
|
||||
// const b = s.vaultBalances[props.vault][props.token.address];
|
||||
return b === undefined ? 0n : BigInt(b)
|
||||
})
|
||||
const balanceFloat = computed(() => tokenFloat(props.token, balance.value))
|
||||
const balanceFloat = computed(() => {
|
||||
let balance = tokenFloat(props.token, s.getBalance(props.token?.a))
|
||||
balance /= 10**props.token.d
|
||||
console.log('balanceFloat', balance, s.getBalance(props.token?.a), props.token)
|
||||
return balance
|
||||
})
|
||||
const floatAmount = ref(0)
|
||||
|
||||
function withdraw() {
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import DCABuilder from "@/components/chart/DCABuilder.vue";
|
||||
import DateBuilder from "@/components/chart/DateBuilder.vue";
|
||||
import LimitBuilder from "@/components/chart/LimitBuilder.vue";
|
||||
import MarketBuilder from "@/components/chart/MarketBuilder.vue";
|
||||
import DiagonalBuilder from "@/components/chart/DiagonalBuilder.vue";
|
||||
import DCABuilder from "@/components/chart/DCABuilder.vue";
|
||||
|
||||
const props = defineProps(['order', 'builder'])
|
||||
|
||||
@@ -21,6 +22,8 @@ const component = computed(()=>{
|
||||
return LimitBuilder
|
||||
case 'DiagonalBuilder':
|
||||
return DiagonalBuilder
|
||||
case 'DateBuilder':
|
||||
return DateBuilder
|
||||
default:
|
||||
console.error('Unknown builder component '+props.builder.component)
|
||||
return null
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
<template>
|
||||
<row-bar :color="builder.color">
|
||||
<color-band :color="builder.color"/>
|
||||
<v-sheet dense style="overflow-y: hidden" class="pa-1 pb-2">
|
||||
<h3 class="ml-1">{{name}}</h3>
|
||||
<div class="d-flex flex-row align-content-stretch">
|
||||
<div class="ml-2"> </div>
|
||||
<slot/>
|
||||
<div class="align-self-center">
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn variant="plain" v-bind="props" icon="mdi-dots-vertical"/>
|
||||
</template>
|
||||
<v-list>
|
||||
<!-- <v-list-subheader :title="'Limit '+ (priceA?priceA.toPrecision(5):'')"/>-->
|
||||
<v-list-item title="Delete" key="withdraw" value="withdraw" prepend-icon="mdi-delete" color="red"
|
||||
@click="deleteMyBuilder"/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<div class="align-self-center ml-auto mr-3 trashcan">
|
||||
<v-btn icon="mdi-delete" @click="showDeleteDialog=true"/>
|
||||
</div>
|
||||
</row-bar>
|
||||
</div>
|
||||
<v-dialog v-model="showDeleteDialog" :max-width="300">
|
||||
<v-card :title="`Delete ${name}?`" :text="`Do you want to delete this ${name}?`">
|
||||
<v-card-actions>
|
||||
<v-btn @click="showDeleteDialog=false">Keep</v-btn>
|
||||
<v-btn variant="tonal" color="error" @click="deleteMyBuilder">Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {builderFuncs, deleteBuilder} from "@/orderbuild.js";
|
||||
import ColorBand from "@/components/chart/ColorBand.vue";
|
||||
import RowBar from "@/components/chart/RowBar.vue";
|
||||
import {onBeforeUnmount, onMounted, onUnmounted, onUpdated, watchEffect} from "vue";
|
||||
import {onBeforeUnmount, onMounted, onUnmounted, onUpdated, ref, watchEffect} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
order: Object,
|
||||
builder: Object,
|
||||
buildTranches: {type: Function},
|
||||
@@ -32,6 +35,7 @@ const props = defineProps({
|
||||
})
|
||||
const emit = defineEmits(['update:builder'])
|
||||
|
||||
const showDeleteDialog = ref(false)
|
||||
|
||||
let lastId = props.builder.id
|
||||
builderFuncs[props.builder.id] = props.buildTranches
|
||||
@@ -53,6 +57,7 @@ if (props.deleteShapes)
|
||||
onBeforeUnmount(props.deleteShapes)
|
||||
|
||||
function deleteMyBuilder() {
|
||||
showDeleteDialog.value = false;
|
||||
deleteBuilder(props.order, props.builder);
|
||||
}
|
||||
|
||||
@@ -60,5 +65,11 @@ function deleteMyBuilder() {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/vars";
|
||||
|
||||
.trashcan {
|
||||
:hover {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<div ref="element" class="chart"/>
|
||||
<div id="tv-widget" ref="element" class="chart"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// import "/public/datafeeds/udf/dist/bundle.js"
|
||||
import {onMounted, ref} from "vue";
|
||||
import {initWidget} from "@/charts/chart.js";
|
||||
|
||||
@@ -12,18 +11,8 @@ const element = ref()
|
||||
onMounted(() => {
|
||||
const el = element.value;
|
||||
initWidget(el)
|
||||
initShapes()
|
||||
})
|
||||
|
||||
function initShapes() {
|
||||
// const c = widget.chart()
|
||||
// for( const s of ss.shapes ) {
|
||||
// const type = s.type.toLowerCase().replace(' ','_')
|
||||
// console.log('create type', type)
|
||||
// c.createMultipointShape(s.points, {shape:type})
|
||||
// }
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,63 +1,55 @@
|
||||
|
||||
<template>
|
||||
<row-bar :color="color">
|
||||
<color-band :color="color"/>
|
||||
<row-bar :color="sideColor">
|
||||
<div style="width: 100%" class="justify-start align-content-start">
|
||||
<v-text-field type="number" inputmode="numeric" pattern="[0-9]*\.?[0-9]*" v-model="order.amount" variant="outlined"
|
||||
density="compact"
|
||||
:hint="available" :persistent-hint="true"
|
||||
min="0"
|
||||
class="amount py-2" :color="color"
|
||||
:label="order.amountIsTokenA ? 'Amount':('Value in '+co.selectedSymbol.quote.s)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-btn variant="outlined" :color="color" class="ml-3"
|
||||
:text="(order.buy ? 'Buy ' : 'Sell ') + co.selectedSymbol.base.s"
|
||||
@click="order.buy=!order.buy"/>
|
||||
</template>
|
||||
<template #prepend-inner>
|
||||
<v-btn variant="text" text="max" class="px-0" size="small"
|
||||
:disabled="!maxAmount || order.amountIsTokenA===order.buy && !co.price" @click="setMax"/>
|
||||
</template>
|
||||
<template #append-inner>
|
||||
<v-btn :text="order.amountIsTokenA?co.baseToken.s:co.quoteToken.s+' worth'"
|
||||
:color="color" variant="text" @click="toggleAmountToken"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<order-amount :order="props.order" class="mt-2" :color="sideColor"/>
|
||||
<template v-for="b in builders">
|
||||
<builder-factory :order="order" :builder="b"/>
|
||||
</template>
|
||||
<div class="my-3">
|
||||
<div v-if="order.builders.length===0"> <!--todo remove gralpha limitation of one builder-->
|
||||
<v-tooltip text="Spread order across time" location="top">
|
||||
<v-tooltip text="Up to 1000 equal parts spread across time" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<span v-bind="props">
|
||||
<v-btn :color="color" variant="text" prepend-icon="mdi-clock-outline" @click="build(order,'DCABuilder')">DCA</v-btn>
|
||||
<v-btn id="DCA-button" :class="order.buy?'green':'red'" variant="text" prepend-icon="mdi-clock-outline" @click="build(order,'DCABuilder')">DCA</v-btn>
|
||||
</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Trade a price level" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<span v-bind="props">
|
||||
<v-btn :color="color" variant="text" prepend-icon="mdi-ray-vertex" @click="build(order,'LimitBuilder')">Limit</v-btn>
|
||||
<v-btn id="LimitBuilder-button" :class="order.buy?'green':'red'" variant="text" prepend-icon="mdi-ray-vertex" @click="build(order,'LimitBuilder')">Limit</v-btn>
|
||||
</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Trade trends and channels" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<span v-bind="props">
|
||||
<v-btn :color="color" variant="text" prepend-icon="mdi-vector-line" @click="build(order,'DiagonalBuilder')">Diagonal</v-btn>
|
||||
<v-btn id="Diagonal-button" :class="order.buy?'green':'red'" variant="text" prepend-icon="mdi-vector-line" @click="build(order,'DiagonalBuilder')">Diagonal</v-btn>
|
||||
</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Up to 10 weighted parts spaced out in time" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<span v-bind="props">
|
||||
<v-btn id="Dates-button" :class="order.buy?'green':'red'" variant="text" prepend-icon="mdi-reorder-vertical" @click="build(order,'DateBuilder')">Dates</v-btn>
|
||||
</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Coming Soon! Stoplosses and Takeprofits" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<span v-bind="props">
|
||||
<v-btn :color="color" variant="text" prepend-icon="mdi-plus-minus" disabled>Stoploss</v-btn>
|
||||
<v-btn :class="order.buy?'green':'red'" variant="text" prepend-icon="mdi-plus-minus" disabled>Stoploss</v-btn>
|
||||
</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<!-- mdi-ray-start-end mdi-vector-polyline -->
|
||||
|
||||
<!-- after="newbie"-->
|
||||
<one-time-hint name="choose-builder" :activator="hintData.activator"
|
||||
:text="hintData.text" location="top"
|
||||
:when="!builtAny"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,29 +60,34 @@
|
||||
|
||||
import BuilderFactory from "@/components/chart/BuilderFactory.vue";
|
||||
import {builderFuncs, newBuilder, orderFuncs, useChartOrderStore} from "@/orderbuild.js";
|
||||
import {useOrderStore, useStore} from "@/store/store.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {computed, onUnmounted, onUpdated, ref, watchEffect} from "vue";
|
||||
import {lightenColor, lightenColor2} from "@/misc.js";
|
||||
import {toPrecision} from "@/misc.js";
|
||||
import {useTheme} from "vuetify";
|
||||
import RowBar from "@/components/chart/RowBar.vue";
|
||||
import ColorBand from "@/components/chart/ColorBand.vue";
|
||||
import Color from "color";
|
||||
import {newOrder} from "@/blockchain/orderlib.js";
|
||||
import OrderAmount from "@/components/chart/OrderAmount.vue";
|
||||
import {track} from "@/track.js";
|
||||
import OneTimeHint from "@/components/OneTimeHint.vue";
|
||||
|
||||
const props = defineProps(['order'])
|
||||
const s = useStore()
|
||||
const co = useChartOrderStore()
|
||||
const os = useOrderStore()
|
||||
|
||||
const marketBuilder = newBuilder('MarketBuilder')
|
||||
|
||||
console.log('chart order', props.order)
|
||||
const builders = computed(()=>props.order.builders.length > 0 ? props.order.builders : [marketBuilder])
|
||||
const tokenIn = computed(()=>props.order.buy ? co.quoteToken : co.baseToken)
|
||||
const tokenOut = computed(()=>props.order.buy ? co.baseToken : co.quoteToken)
|
||||
|
||||
|
||||
console.log('order', props.order)
|
||||
const builtAny = ref(false)
|
||||
|
||||
function build(order, component, options={}) {
|
||||
track('build', {builder:component})
|
||||
builtAny.value = true
|
||||
order.builders.push(newBuilder(component, options))
|
||||
}
|
||||
|
||||
@@ -114,7 +111,38 @@ function buildOrder() {
|
||||
const order = props.order
|
||||
console.log('buildOrder', order)
|
||||
|
||||
if (!order.amount) return null
|
||||
if (!order.amount)
|
||||
return {order: null, warnings: ['Amount must be greater than 0']}
|
||||
|
||||
const symbol = co.selectedSymbol
|
||||
const amountDec = order.amountIsTokenA ? symbol.base.d : symbol.quote.d
|
||||
const amount = BigInt(Math.trunc(order.amount * 10 ** amountDec))
|
||||
|
||||
let warnings = []
|
||||
const inAddr = order.buy ? symbol.quote.a : symbol.base.a
|
||||
const inDec = order.buy ? symbol.quote.d : symbol.base.d
|
||||
const available = s.getBalance(inAddr) / 10 ** inDec
|
||||
|
||||
let needed
|
||||
if (order.amountIsTokenA && order.buy)
|
||||
// need quote currency to buy an amount of base
|
||||
needed = order.amount * co.price
|
||||
else if (order.amountIsTokenA && !order.buy)
|
||||
// sell an exact amount of base
|
||||
needed = order.amount
|
||||
else if (!order.amountIsTokenA && order.buy)
|
||||
// need an exact amount of quote
|
||||
needed = order.amount
|
||||
else if (!order.amountIsTokenA && !order.buy)
|
||||
// sell a quote amount worth of base
|
||||
needed = order.amount / co.price
|
||||
else
|
||||
throw new Error('Invalid order')
|
||||
const deficit = needed - available
|
||||
if (deficit > 0) {
|
||||
const inSymbol = order.buy ? symbol.quote.s : symbol.base.s
|
||||
warnings.push(`Insufficient funds. Add ${toPrecision(deficit, 5)} ${inSymbol} to your vault to complete this order.`)
|
||||
}
|
||||
|
||||
// struct SwapOrder {
|
||||
// address tokenIn;
|
||||
@@ -127,20 +155,24 @@ function buildOrder() {
|
||||
// uint64 conditionalOrder; // use NO_CONDITIONAL_ORDER for no chaining. conditionalOrder index must be < than this order's index for safety (written first) and conditionalOrder state must be Template
|
||||
// Tranche[] tranches;
|
||||
// }
|
||||
const symbol = co.selectedSymbol
|
||||
const amountDec = order.amountIsTokenA ? symbol.base.d : symbol.quote.d
|
||||
const amount = BigInt(Math.trunc(order.amount * 10 ** amountDec))
|
||||
const amountIsInput = !!(order.amountIsTokenA ^ order.buy)
|
||||
|
||||
let tranches = []
|
||||
for (const builder of builders.value) {
|
||||
console.log('builder', builder)
|
||||
const ts = builderFuncs[builder.id]()
|
||||
const built = builderFuncs[builder.id]()
|
||||
const ts = built.tranches
|
||||
const ws = built.warnings
|
||||
console.log('tranches', ts)
|
||||
tranches = [...tranches, ...ts]
|
||||
warnings = [...warnings, ...ws]
|
||||
}
|
||||
|
||||
return newOrder(tokenIn.value.a, tokenOut.value.a, symbol.exchangeId, symbol.fee, amount, amountIsInput, symbol.inverted, tranches)
|
||||
return {
|
||||
warnings,
|
||||
order: newOrder(tokenIn.value.a, tokenOut.value.a, symbol.exchangeId, symbol.fee,
|
||||
amount, amountIsInput, symbol.inverted, tranches),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +188,8 @@ onUnmounted(() => delete orderFuncs[lastId])
|
||||
|
||||
|
||||
const theme = useTheme().current
|
||||
const color = computed(()=>new Color(props.order.buy?theme.value.colors.success:theme.value.colors.error).darken(0.2).string())
|
||||
const sideColor = computed(()=>new Color(props.order.buy?theme.value.colors.success:theme.value.colors.error).darken(0.2).string())
|
||||
const color = computed(()=>theme.value.colors["on-background"])
|
||||
// const lightColor = computed(() => lightenColor(color.value))
|
||||
// const faintColor = computed(() => lightenColor2(color.value))
|
||||
// const colorStyle = computed(() => { return {'color': color.value} })
|
||||
@@ -164,55 +197,23 @@ const color = computed(()=>new Color(props.order.buy?theme.value.colors.success:
|
||||
// const lightColorStyle = computed(() => { return {'background-color': lightColor.value} })
|
||||
// const faintColorStyle = computed(() => { return {'background-color': faintColor.value} })
|
||||
|
||||
const inToken = computed( ()=>props.order.buy ? co.quoteToken : co.baseToken )
|
||||
const maxAmount = computed(()=>{
|
||||
const token = inToken.value;
|
||||
if (!token)
|
||||
return null
|
||||
const balance = s.balances[token.a]
|
||||
if( !balance )
|
||||
return null
|
||||
const divisor = os.amountIsTotal ? 1 : os.tranches
|
||||
return balance / 10**token.d / divisor
|
||||
})
|
||||
const available = computed(()=>{
|
||||
const max = maxAmount.value
|
||||
return max === null ? '' : `Available: ${maxAmount.value} ${tokenIn.value.s}`
|
||||
})
|
||||
// Tutorial Hint
|
||||
|
||||
const lastMaxValue = ref(-1)
|
||||
let tutorial = 'limit'
|
||||
|
||||
function setMax() {
|
||||
let amount = maxAmount.value
|
||||
if (amount) {
|
||||
const order = props.order
|
||||
const price = co.price
|
||||
if (order.amountIsTokenA===order.buy) {
|
||||
if (order.buy)
|
||||
amount /= price
|
||||
else
|
||||
amount *= price
|
||||
}
|
||||
amount = Number(amount.toPrecision(7))
|
||||
lastMaxValue.value = amount
|
||||
order.amount = amount
|
||||
}
|
||||
const _hintData = {
|
||||
'limit': {
|
||||
activator: '#LimitBuilder-button',
|
||||
text: '↓ Try a Limit Ladder ↓'
|
||||
},
|
||||
}
|
||||
|
||||
function toggleAmountToken() {
|
||||
const order = props.order
|
||||
order.amountIsTokenA=!order.amountIsTokenA
|
||||
if (order.amount === lastMaxValue.value)
|
||||
setMax()
|
||||
}
|
||||
const hintData = computed(()=>_hintData[tutorial] || _hintData['limit'])
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.amount {
|
||||
max-width: 30em;
|
||||
div.v-field {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
@use "src/styles/vars" as *;
|
||||
.v-btn.green:hover {color: $green !important;}
|
||||
.v-btn.red:hover {color: $red !important;}
|
||||
</style>
|
||||
@@ -7,7 +7,13 @@
|
||||
Place Dexorder
|
||||
</v-btn>
|
||||
<v-btn variant="text" prepend-icon="mdi-delete" v-if="co.orders.length>0"
|
||||
:disabled="!orderChanged" @click="cancelOrder">Reset</v-btn>
|
||||
:disabled="!orderChanged" @click="resetOrder">Reset</v-btn>
|
||||
<v-btn id="share-btn" variant="text" prepend-icon="mdi-share" v-if="co.orders.length>0"
|
||||
:disabled="sharing"
|
||||
@click="shareOrder">{{sharing?'Preparing...':'Share Order'}}</v-btn>
|
||||
<v-tooltip activator="#share-btn" :text="shareError?'Error copying share link :(':'Copied share link!'" v-model="showSharedTooltip"
|
||||
:open-on-hover="false" :open-on-click="false"
|
||||
/>
|
||||
</template>
|
||||
<div class="overflow-y-auto">
|
||||
<needs-chart>
|
||||
@@ -19,7 +25,30 @@
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn @click="()=>showResetDialog=false">Keep Existing</v-btn>
|
||||
<v-btn @click="()=>{co.resetOrders(); showResetDialog=false}" color="red" text="Reset Order"/>
|
||||
<v-btn @click="doResetOrder" color="red" text="Reset Order"/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showWarnings" max-width="300">
|
||||
<v-card prepend-icon="mdi-warning" title="Order Warnings">
|
||||
<v-card-item>
|
||||
<ul class="ml-5">
|
||||
<li v-for="w of orderWarnings">{{w}}</li>
|
||||
</ul>
|
||||
</v-card-item>
|
||||
<v-card-text>Continue placing this order?</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn @click="()=>showWarnings=false">Back</v-btn>
|
||||
<v-btn @click="doPlaceOrder">Place Order</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="placementError" max-width="300">
|
||||
<v-card prepend-icon="mdi-alert" title="Order Error" text="There was an error placing your order. Please try again or contact support.">
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn @click="()=>{placementError=false;ws.transaction=null}">OK</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -41,6 +70,9 @@ import {useWalletStore} from "@/blockchain/wallet.js";
|
||||
import ToolbarPane from "@/components/chart/ToolbarPane.vue";
|
||||
import NeedsChart from "@/components/NeedsChart.vue";
|
||||
import {PlaceOrderTransaction} from "@/blockchain/transaction.js";
|
||||
import {errorSuggestsMissingVault} from "@/misc.js";
|
||||
import {track} from "@/track.js";
|
||||
import {getShareUrl} from "@/share.js";
|
||||
|
||||
const s = useStore()
|
||||
const co = useChartOrderStore()
|
||||
@@ -48,10 +80,10 @@ const os = useOrderStore()
|
||||
const ws = useWalletStore()
|
||||
|
||||
function changeSymbol(symbol) {
|
||||
console.log('changeSymbol', symbol)
|
||||
// console.log('changeSymbol', symbol)
|
||||
os.tokenA = symbol.base
|
||||
os.tokenB = symbol.quote
|
||||
routeFinder.invoke()
|
||||
// routeFinder.invoke()
|
||||
}
|
||||
|
||||
|
||||
@@ -78,10 +110,23 @@ const valid = computed(()=>{
|
||||
|
||||
const orderChanged = computed(()=>!(co.orders.length===1 && co.orders[0].builders.length===0 && !co.orders[0].amount))
|
||||
|
||||
function cancelOrder() {
|
||||
const showWarnings = ref(false)
|
||||
const orderWarnings = ref([])
|
||||
|
||||
const placementError = ref(false)
|
||||
|
||||
function resetOrder() {
|
||||
showResetDialog.value = true
|
||||
}
|
||||
|
||||
function doResetOrder() {
|
||||
co.resetOrders();
|
||||
orderWarnings.value = []
|
||||
showWarnings.value = false
|
||||
showResetDialog.value = false
|
||||
placementError.value = false
|
||||
}
|
||||
|
||||
watchEffect(()=>{
|
||||
const removable = []
|
||||
for (const order of ws.pendingOrders) {
|
||||
@@ -108,23 +153,90 @@ watchEffect(()=>{
|
||||
}
|
||||
})
|
||||
|
||||
let built = []
|
||||
|
||||
async function placeOrder() {
|
||||
track('place-order')
|
||||
const chartOrders = co.orders;
|
||||
const built = []
|
||||
const allWarns = []
|
||||
built = []
|
||||
for (const chartOrder of chartOrders) {
|
||||
console.log('chartOrder', chartOrder)
|
||||
const buildOrder = orderFuncs[chartOrder.id]
|
||||
const order = buildOrder()
|
||||
const {order, warnings} = buildOrder()
|
||||
built.push(order)
|
||||
allWarns.push(...warnings)
|
||||
}
|
||||
console.log('place orders', built)
|
||||
|
||||
if (allWarns.length > 0) {
|
||||
orderWarnings.value = allWarns
|
||||
showWarnings.value = true
|
||||
return
|
||||
}
|
||||
|
||||
await doPlaceOrder()
|
||||
}
|
||||
|
||||
async function doPlaceOrder() {
|
||||
console.log('place orders')
|
||||
placementError.value = false
|
||||
showWarnings.value = false
|
||||
if (ws.transaction!==null) {
|
||||
console.error('Transaction already in progress')
|
||||
}
|
||||
else {
|
||||
new PlaceOrderTransaction(s.chainId, toRaw(built[0])).submit()
|
||||
const tx = new PlaceOrderTransaction(s.chainId, toRaw(built[0]));
|
||||
tx.retries = 60
|
||||
const oldFailed = tx.failed
|
||||
tx.failed = function(e) {
|
||||
console.error('place order failed', errorSuggestsMissingVault(e), e.code, e)
|
||||
if (errorSuggestsMissingVault(e) && e.action === 'feeManager' && this.retries-- >= 0) {
|
||||
s.creatingVault = true
|
||||
}
|
||||
else {
|
||||
s.creatingVault = false
|
||||
oldFailed.bind(this)(e)
|
||||
placementError.value = e.info?.error?.code !== 4001
|
||||
}
|
||||
}
|
||||
tx.submit() // this assigns the tx to walletStore.transaction
|
||||
}
|
||||
}
|
||||
|
||||
const sharing = ref(false)
|
||||
const showSharedTooltip = ref(false)
|
||||
const shareError = ref(false)
|
||||
const showShareDialog = ref(false)
|
||||
const shareUrl = ref(null)
|
||||
|
||||
function shareOrder() {
|
||||
sharing.value = true
|
||||
track('share')
|
||||
getShareUrl().then(url => {
|
||||
shareError.value = url === null
|
||||
if (url === null) {
|
||||
sharing.value = false
|
||||
return
|
||||
}
|
||||
console.log('share url', url)
|
||||
shareUrl.value = url
|
||||
sharing.value = false
|
||||
navigator.permissions.query({name: "clipboard-write"}).then((permission) => {
|
||||
const permitted = permission.state === "granted" || permission.state === "prompt"
|
||||
if (!permitted) {
|
||||
showShareDialog.value = true
|
||||
} else {
|
||||
navigator.clipboard.writeText(url)
|
||||
.then(() => {
|
||||
showSharedTooltip.value = true
|
||||
setTimeout(() => showSharedTooltip.value = false, 3000)
|
||||
})
|
||||
.catch(() => {
|
||||
showShareDialog.value = true
|
||||
})
|
||||
}
|
||||
}).catch(() => null)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<toolbar-pane title="Status" icon="mdi-format-list-bulleted-square">
|
||||
<template #toolbar>
|
||||
<v-btn variant="text" v-if="s.vault" prepend-icon="mdi-delete-alert" color="red"
|
||||
@click="cancelAll(s.vault)" :disabled="!anyOrdersOpen"
|
||||
@click="(async function (vault){
|
||||
new CancelAllTransaction(useStore().chainId, vault).submit()
|
||||
})(s.vault)" :disabled="!anyOrdersOpen"
|
||||
text="Cancel All"/>
|
||||
</template>
|
||||
<needs-signer>
|
||||
@@ -16,10 +18,10 @@
|
||||
import ToolbarPane from "@/components/chart/ToolbarPane.vue";
|
||||
import Orders from "@/components/Status.vue";
|
||||
import NeedsSigner from "@/components/NeedsSigner.vue";
|
||||
import {cancelAll} from "@/blockchain/wallet.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {computed} from "vue";
|
||||
import {isOpen} from "@/blockchain/orderlib.js";
|
||||
import {CancelAllTransaction} from "@/blockchain/transaction.js";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
|
||||
@@ -1,243 +1,300 @@
|
||||
<template>
|
||||
<rung-builder name='DCA' :order="order" :builder="builder" v-model="timeEndpoints"
|
||||
:shape="VLine"
|
||||
:mode="1" :flip="flipped" :orientation="0"
|
||||
:get-model-value="getModelValue" :set-model-value="setModelValue"
|
||||
:get-points-value="getPointsValue"
|
||||
:set-values="setValues" :set-weights="setWeights"
|
||||
:std-width="stdWidth" :build-tranches="buildTranches">
|
||||
<template class="d-flex align-content-center flex-column" style="height: 100%; width: 100%;">
|
||||
<builder-panel :order="order" :builder="builder" :build-tranches="buildTranches"
|
||||
:adjust-shapes="adjustShapes" :delete-shapes="deleteShapes" name="DCA">
|
||||
<div class="d-flex flex-column" style="width: 100%;">
|
||||
<div class="d-flex flex-row mb-5">
|
||||
<div class="align-self-center mr-3">Start:</div>
|
||||
<absolute-time-entry v-model="startTime"/>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<v-list style="background-color: inherit">
|
||||
<v-list-item v-for="t in absoluteTimes">{{t}}</v-list-item>
|
||||
</v-list>
|
||||
-->
|
||||
<div class="d-flex flex-row">
|
||||
<div>
|
||||
<v-text-field label="Split into" type="number" variant="outlined"
|
||||
aria-valuemin="1" aria-valuemax="100" min="1" max="1000" step="1"
|
||||
:hint="partsGasHint" :persistent-hint="true"
|
||||
:color="color"
|
||||
v-model="parts" v-auto-select class="parts mr-3">
|
||||
<template v-slot:append-inner>
|
||||
parts
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<absolute-time-entry v-model="absTimeA"/>
|
||||
</td>
|
||||
<td class="weight">{{ weights.length ? allocationTexts[0] : '' }}</td>
|
||||
</tr>
|
||||
<tr v-if="weights.length>2" v-for="i in weights.length-2" class="ml-5"> <!-- vue uses 1-based loops -->
|
||||
<td class="d-flex justify-end pr-3">{{ dateStrings[i] }}</td>
|
||||
<td class="weight">{{ allocationTexts[i] }}</td>
|
||||
</tr>
|
||||
<tr v-if="weights.length>1">
|
||||
<td>
|
||||
<absolute-time-entry v-model="absTimeB"/>
|
||||
</td>
|
||||
<td class="weight">{{ allocationTexts[weights.length-1] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mr-3">
|
||||
<order-amount :order="props.order" :multiplier="props.builder.tranches" :color="color" style="width: 20em"/>
|
||||
</div>
|
||||
|
||||
|
||||
</rung-builder>
|
||||
<div>
|
||||
<v-text-field type="number" variant="outlined" :min="1" v-model="displayedInterval" class="interval"
|
||||
:color="color"
|
||||
:label="intervalIsTotal ? 'Total completion time' : 'Time between parts'" v-auto-select>
|
||||
<template v-slot:prepend-inner>
|
||||
<v-btn variant="outlined"
|
||||
:text="intervalIsTotal ? 'All within' : 'Spaced apart by'" class="within mr-2"
|
||||
style="width: 10em"
|
||||
@click="intervalIsTotal=!intervalIsTotal"/>
|
||||
</template>
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" :text="timeUnitsStr" style="width: 5em"
|
||||
@click="toggleTimeUnits" class="time-units"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</builder-panel>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {builderDefaults, DEFAULT_SLIPPAGE, MIN_EXECUTION_TIME, useChartOrderStore} from "@/orderbuild.js";
|
||||
import {allocationText, VLine} from "@/charts/shape.js";
|
||||
import {sideColor} from "@/misc.js";
|
||||
import {useOrderStore, useStore} from "@/store/store.js";
|
||||
import {DISTANT_FUTURE, MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
|
||||
import RungBuilder from "@/components/chart/RungBuilder.vue";
|
||||
import {builderDefaults, useChartOrderStore} from "@/orderbuild.js";
|
||||
import {allocationText, ShapeType} from "@/charts/shape.js";
|
||||
import {sideColor, SingletonCoroutine, toPrecision, vAutoSelect} from "@/misc.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {computed, ref, watchEffect} from "vue";
|
||||
import {chart, dragging} from "@/charts/chart.js";
|
||||
import {createShape, deleteShapeId, dragging, widget} from "@/charts/chart.js";
|
||||
import AbsoluteTimeEntry from "@/components/AbsoluteTimeEntry.vue";
|
||||
import {DateTime} from "luxon";
|
||||
import BuilderPanel from "@/components/chart/BuilderPanel.vue";
|
||||
import {ohlcStart} from "@/charts/chart-misc.js";
|
||||
import Color from "color";
|
||||
import OrderAmount from "@/components/chart/OrderAmount.vue";
|
||||
import {DEFAULT_SLIPPAGE, MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
|
||||
import {getFeeSchedule} from "@/fees.js";
|
||||
import {NATIVE_TOKEN} from "@/common.js";
|
||||
|
||||
const s = useStore()
|
||||
const os = useOrderStore()
|
||||
const co = useChartOrderStore()
|
||||
const props = defineProps(['order', 'builder'])
|
||||
const emit = defineEmits(['update:builder'])
|
||||
|
||||
const minWidth = computed(()=>co.intervalSecs)
|
||||
const stdWidth = computed(()=>10 * minWidth.value)
|
||||
|
||||
function computeDefaultColor() {
|
||||
const index = props.order.builders.indexOf(props.builder)
|
||||
return sideColor(props.order.buy, index)
|
||||
}
|
||||
|
||||
const defaultColor = computeDefaultColor()
|
||||
const color = computed(()=>computeDefaultColor())
|
||||
|
||||
const defaultTranches = 10
|
||||
|
||||
builderDefaults(props.builder, {
|
||||
timeA: s.clock, // todo relative 0
|
||||
timeB: null,
|
||||
startTime: s.clock, // todo relative 0
|
||||
endTime: s.clock + defaultTranches * co.intervalSecs,
|
||||
interval: co.intervalSecs,
|
||||
tranches: defaultTranches,
|
||||
percentage: 100/defaultTranches,
|
||||
// relative: true, // todo
|
||||
relative: false,
|
||||
slippage: DEFAULT_SLIPPAGE,
|
||||
rungs: 1,
|
||||
skew: 0,
|
||||
color: defaultColor,
|
||||
color: color.value,
|
||||
valid: true,
|
||||
})
|
||||
|
||||
const times = ref([])
|
||||
const weights = ref([])
|
||||
|
||||
const amountSymbol = computed(()=>props.order.amountIsTokenA ? co.selectedSymbol.base.s : co.selectedSymbol.quote.s )
|
||||
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(w, w*props.order.amount, amountSymbol.value)))
|
||||
|
||||
const endTimes = computed(()=>{
|
||||
if (props.builder.rungs === 1)
|
||||
return DISTANT_FUTURE
|
||||
const ts = times.value
|
||||
const window = Math.max(MIN_EXECUTION_TIME, Math.floor((ts[ts.length-1]-ts[0])/props.builder.rungs))
|
||||
return ts.map((t)=>t+window)
|
||||
})
|
||||
const absoluteTimes = computed(()=>{
|
||||
// console.log('absoluteTimes', props.builder.relative, times.value)
|
||||
// if (!props.builder.relative)
|
||||
return times.value
|
||||
// const now = s.clock
|
||||
// return times.value.map((t)=>now+t)
|
||||
})
|
||||
|
||||
const dateStrings = computed(()=>absoluteTimes.value.map((t)=>{
|
||||
const n = DateTime.fromSeconds(t).setZone(s.timeZone)
|
||||
const y = n.toLocaleString({year:'numeric'})
|
||||
const m = n.toLocaleString({month:'long'})
|
||||
const d = n.toLocaleString({day:'numeric'})
|
||||
const h = n.toLocaleString({hour:'numeric', minute:'numeric'})
|
||||
return `${y} ${m} ${d} ${h}`
|
||||
}))
|
||||
|
||||
watchEffect(()=>{
|
||||
// auto scroll
|
||||
if (!dragging && absoluteTimes.value.length) {
|
||||
const endTime = absoluteTimes.value[absoluteTimes.value.length-1]
|
||||
const range = chart.getVisibleRange()
|
||||
const width = range.to - range.from
|
||||
const now = s.clock
|
||||
const extra = (Math.max(0,endTime - now) + stdWidth.value/2) / width
|
||||
// console.log('visrange', range, width, absV)
|
||||
if (range.to < endTime) {
|
||||
// console.log('scrolling')
|
||||
chart.setVisibleRange({from: now - width, to: now}, {
|
||||
percentRightMargin: Math.round(100 * extra),
|
||||
applyDefaultRightMargin: false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const absTimeA = computed({
|
||||
get() { return _timeEndpoints.value[0] },
|
||||
set(v) {
|
||||
if (v!==null)
|
||||
v = Number(v)
|
||||
updateA(v)
|
||||
}
|
||||
})
|
||||
const absTimeB = computed({
|
||||
get() { return _timeEndpoints.value[1] },
|
||||
set(v) {
|
||||
if (v!==null)
|
||||
v = Number(v)
|
||||
updateB(v)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const _timeEndpoints = ref([props.builder.timeA, props.builder.timeB])
|
||||
const timeEndpoints = computed({
|
||||
get() { return _timeEndpoints.value},
|
||||
set(v) {
|
||||
const [a, b] = v
|
||||
update(a,b)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function updateA(a) {
|
||||
update(a, _timeEndpoints.value[1], true)
|
||||
}
|
||||
|
||||
|
||||
function updateB(b) {
|
||||
update(_timeEndpoints.value[0], b, false)
|
||||
}
|
||||
|
||||
|
||||
function update(a, b, updatingA) {
|
||||
if (updatingA) {
|
||||
const minB = a + minWidth.value
|
||||
if (b < minB)
|
||||
b = minB
|
||||
}
|
||||
else {
|
||||
const maxA = b - minWidth.value
|
||||
if (a > maxA)
|
||||
a = maxA
|
||||
}
|
||||
_timeEndpoints.value = [a, b]
|
||||
const newBuilder = {...props.builder}
|
||||
newBuilder.timeA = a
|
||||
newBuilder.timeB = b
|
||||
emit('update:builder', newBuilder)
|
||||
}
|
||||
|
||||
const flipped = computed(()=>{
|
||||
const a = props.builder.timeA
|
||||
const b = props.builder.timeB
|
||||
return a !== null && b !== null && a > b
|
||||
})
|
||||
function adjustShapes() {}
|
||||
|
||||
function buildTranches() {
|
||||
const order = props.order
|
||||
const builder = props.builder
|
||||
const tranches = []
|
||||
|
||||
console.log('buildTranches', builder, order, tranches)
|
||||
const ts = times.value
|
||||
const ets = endTimes.value
|
||||
const ws = weights.value
|
||||
for(let i=0; i<ts.length; i++) {
|
||||
const t = newTranche({
|
||||
fraction: ws[i] * MAX_FRACTION,
|
||||
startTime: ts[i],
|
||||
endTime: Math.max(ets[i],ts[i]+60), // always give at least 60 seconds of window to execute
|
||||
slippage: builder.slippage,
|
||||
})
|
||||
tranches.push(t)
|
||||
const warnings = []
|
||||
let interval = props.builder.interval
|
||||
let parts = props.builder.tranches
|
||||
if (interval < 60) {
|
||||
interval = 60
|
||||
parts = (props.builder.endTime - props.builder.startTime) / interval
|
||||
warnings.push(`DCA parts cannot be closer than one minute apart. Using ${parts} parts.`)
|
||||
}
|
||||
return tranches
|
||||
const rate = Math.ceil(MAX_FRACTION / parts)
|
||||
const endTime = Math.ceil(props.builder.endTime + 10*props.builder.tranches)
|
||||
const startTime = Math.floor(props.builder.startTime);
|
||||
const tranches = [newTranche({marketOrder:true,
|
||||
startTime, endTime, rateLimitFraction: rate, rateLimitPeriod: Math.floor(interval),
|
||||
slippage: props.builder.slippage})]
|
||||
return {tranches, warnings}
|
||||
}
|
||||
|
||||
const parts = computed({
|
||||
get() { return props.builder.tranches },
|
||||
set(v) {
|
||||
v = Number(v)
|
||||
v = Math.max(1, Math.min(1000,v))
|
||||
props.builder.tranches = v
|
||||
props.builder.endTime = props.builder.startTime + props.builder.interval * v
|
||||
setPoints(null, true)
|
||||
}
|
||||
})
|
||||
|
||||
function getModelValue(model) {
|
||||
if(!model) {
|
||||
console.log('getModelValue', model)
|
||||
const sched = ref(null)
|
||||
const schedFetcher = new SingletonCoroutine(async (vault)=>sched.value = vault === null ? null : await getFeeSchedule(vault))
|
||||
|
||||
const partsGasHint = computed(()=>{
|
||||
if (sched.value === null) {
|
||||
schedFetcher.invoke(s.vault)
|
||||
return null
|
||||
}
|
||||
return model.time
|
||||
const ethFee = Number(sched.value.gasFee) * parts.value / 1e18;
|
||||
const mark = s.markPrice(NATIVE_TOKEN)
|
||||
if (mark)
|
||||
return '$' + Number(ethFee*mark).toFixed(2) + ' gas fee'
|
||||
else
|
||||
return toPrecision(ethFee) + ' ETH gas fee'
|
||||
})
|
||||
|
||||
const intervalIsTotal = ref(false)
|
||||
const displayedInterval = computed({
|
||||
get() {
|
||||
let result = props.builder.interval / timeUnits[timeUnitIndex.value][1]
|
||||
if (intervalIsTotal.value)
|
||||
// express as per-part intervals
|
||||
result *= props.builder.tranches
|
||||
return result
|
||||
},
|
||||
set(v) {
|
||||
v = Number(v)
|
||||
let newValue = v * timeUnits[timeUnitIndex.value][1];
|
||||
if (intervalIsTotal.value)
|
||||
newValue /= props.builder.tranches
|
||||
if (Math.abs(newValue - props.builder.interval) >= Number.EPSILON) {
|
||||
props.builder.interval = newValue
|
||||
props.builder.endTime = props.builder.startTime + newValue * props.builder.tranches
|
||||
setPoints(null, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const timeUnits = [['minutes', 60], ['hours', 3600], ['days', 86400]]
|
||||
function defaultTimeUnit() {
|
||||
let i=1
|
||||
while( i < timeUnits.length && props.builder.interval >= timeUnits[i][1] )
|
||||
i++
|
||||
console.log('defaultTimeUnit', props.builder.interval, i-1)
|
||||
return i-1
|
||||
}
|
||||
const _timeUnitIndex = ref(defaultTimeUnit());
|
||||
const timeUnitIndex = computed({
|
||||
get() {return _timeUnitIndex.value},
|
||||
set(v) {_timeUnitIndex.value = v % timeUnits.length}
|
||||
})
|
||||
const timeUnitsStr = computed(()=>timeUnits[timeUnitIndex.value][0])
|
||||
|
||||
function toggleTimeUnits() {
|
||||
timeUnitIndex.value += 1
|
||||
}
|
||||
|
||||
function getPointsValue(points) {
|
||||
return points[0].price
|
||||
const startTime = computed({
|
||||
get() { return props.builder.relative ? s.clock + props.builder.startTime : props.builder.startTime },
|
||||
set(v) {emitUpdate({startTime: v})}
|
||||
})
|
||||
|
||||
const endTime = computed({
|
||||
get() { return startTime.value + props.builder.interval * props.builder.tranches },
|
||||
set(v) {
|
||||
emitUpdate({endTime: v, interval: Math.abs(v - startTime.value)})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const barStart = computed(()=>ohlcStart(startTime.value, props.builder.interval))
|
||||
const barEnd = computed(()=>ohlcStart(endTime.value, props.builder.interval))
|
||||
|
||||
function emitUpdatedPoints(a, b) {
|
||||
const updates = {}
|
||||
if (a.time !== barStart.value)
|
||||
updates.startTime = a.time
|
||||
// only set the end if it was moved relative to the start
|
||||
if (b.time - a.time === barEnd.value - barStart.value)
|
||||
// the end has moved exactly the same amount as the start. add a relative amount.
|
||||
updates.endTime = a.time + (endTime.value - startTime.value)
|
||||
else
|
||||
updates.endTime = b.time
|
||||
updates.interval = (b.time - a.time) / props.builder.tranches
|
||||
emitUpdate(updates)
|
||||
}
|
||||
|
||||
function setModelValue(model, value) {
|
||||
// console.log('DCA set model value', model, value)
|
||||
// const v = value === null ? null : props.builder.relative ? s.clock + Math.round(value) : Math.round(value)
|
||||
const v = value === null ? null : Math.round(value)
|
||||
if (model.time !== v) {
|
||||
// console.log('DCA do set time', v)
|
||||
model.time = v
|
||||
function setPoints(points=null, shapeCorrectionNeeded=false) {
|
||||
if (points === null)
|
||||
points = [{time:props.builder.startTime}, {time:props.builder.endTime}]
|
||||
let [a, b] = points
|
||||
const period = co.intervalSecs;
|
||||
|
||||
if (b.time < a.time) {
|
||||
[a, b] = [b, a]
|
||||
shapeCorrectionNeeded = true
|
||||
} else if (b.time === a.time) {
|
||||
b.time = a.time + period
|
||||
shapeCorrectionNeeded = true
|
||||
}
|
||||
|
||||
const curBarStart = ohlcStart(s.clock, period);
|
||||
if (a.time < curBarStart) {
|
||||
const width = b.time - a.time;
|
||||
a.time = s.clock
|
||||
b.time = a.time + width
|
||||
shapeCorrectionNeeded = true
|
||||
}
|
||||
emitUpdatedPoints(a, b);
|
||||
if (shapeCorrectionNeeded) {
|
||||
const [sa, sb] = shape.getPoints()
|
||||
const aTime = ohlcStart(a.time, period);
|
||||
const bTime = ohlcStart(b.time, period);
|
||||
if (sa.time !== aTime || sb.time !== bTime) {
|
||||
const aa = {time: aTime}
|
||||
const bb = {time: bTime}
|
||||
shape.setPoints([aa, bb])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setValues(values) {
|
||||
times.value = values.map((t)=>Math.round(t))
|
||||
const callbacks = {
|
||||
onDrag(shapeId, shape, points) {
|
||||
let [a, b] = points
|
||||
if (b.time < a.time)
|
||||
[a, b] = [b, a]
|
||||
emitUpdatedPoints(a, b)
|
||||
},
|
||||
onPoints(shapeId, shape, points) {
|
||||
setPoints(points, shape);
|
||||
},
|
||||
}
|
||||
|
||||
function setWeights(ws) { weights.value = ws }
|
||||
function emitUpdate(changes) {
|
||||
for (const [k, v] of Object.entries(changes))
|
||||
props.builder[k] = v
|
||||
}
|
||||
|
||||
const text = computed(()=>{
|
||||
const o = props.order
|
||||
return allocationText(o.buy, 1, o.amount, co.selectedSymbol.base.s,
|
||||
o.amountIsTokenA ? null : co.selectedSymbol.quote.s, parts.value, '\n')
|
||||
})
|
||||
|
||||
let shapeId = createShape(ShapeType.DateRange, [{time:barStart.value}, {time:barEnd.value}], {}, callbacks)
|
||||
let shape = widget.activeChart().getShapeById(shapeId)
|
||||
function setProperties() {
|
||||
const color = computeDefaultColor()
|
||||
shape.setProperties({
|
||||
extendTop: true,
|
||||
extendBottom: true,
|
||||
linecolor: color,
|
||||
backgroundColor: new Color(color).alpha(0.2).toString(),
|
||||
backgroundTransparency: 60,
|
||||
customText: {
|
||||
text: text.value,
|
||||
visible: true,
|
||||
color: color,
|
||||
},
|
||||
})
|
||||
}
|
||||
setProperties()
|
||||
|
||||
watchEffect(setProperties)
|
||||
|
||||
watchEffect(()=>{
|
||||
const curBarStart = ohlcStart(s.clock, co.intervalSecs);
|
||||
if (curBarStart > barStart.value && !dragging) { // check dragging late to ensure reactivity on bar start
|
||||
const delta = curBarStart - props.builder.startTime
|
||||
setPoints([{time: props.builder.startTime + delta}, {time: props.builder.endTime + delta}])
|
||||
}
|
||||
})
|
||||
|
||||
function deleteShapes() {
|
||||
deleteShapeId(shapeId)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -246,5 +303,11 @@ td.weight {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.parts {
|
||||
width: 10em;
|
||||
}
|
||||
.interval {
|
||||
width: 22em;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
255
src/components/chart/DateBuilder.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template class="d-flex align-content-center flex-column" style="height: 100%; width: 100%;">
|
||||
<rung-builder name='Dates' :order="order" :builder="builder" v-model="timeEndpoints"
|
||||
:shape="VLine"
|
||||
:mode="1" :flip="flipped" :orientation="0"
|
||||
:get-model-value="getModelValue" :set-model-value="setModelValue"
|
||||
:get-points-value="getPointsValue"
|
||||
:set-values="setValues" :set-weights="setWeights"
|
||||
:std-width="stdWidth" :build-tranches="buildTranches">
|
||||
|
||||
<!--
|
||||
<v-list style="background-color: inherit">
|
||||
<v-list-item v-for="t in absoluteTimes">{{t}}</v-list-item>
|
||||
</v-list>
|
||||
-->
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<absolute-time-entry v-model="absTimeA"/>
|
||||
</td>
|
||||
<td class="weight">{{ weights.length ? allocationTexts[0] : '' }}</td>
|
||||
</tr>
|
||||
<tr v-if="weights.length>2" v-for="i in weights.length-2" class="ml-5"> <!-- vue uses 1-based loops -->
|
||||
<td class="d-flex justify-end pr-3">{{ dateStrings[i] }}</td>
|
||||
<td class="weight">{{ allocationTexts[i] }}</td>
|
||||
</tr>
|
||||
<tr v-if="weights.length>1">
|
||||
<td>
|
||||
<absolute-time-entry v-model="absTimeB"/>
|
||||
</td>
|
||||
<td class="weight">{{ allocationTexts[weights.length-1] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</rung-builder>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {builderDefaults, useChartOrderStore} from "@/orderbuild.js";
|
||||
import {allocationText, VLine} from "@/charts/shape.js";
|
||||
import {sideColor} from "@/misc.js";
|
||||
import {useOrderStore, usePrefStore, useStore} from "@/store/store.js";
|
||||
import {DEFAULT_SLIPPAGE, DISTANT_FUTURE, MAX_FRACTION, MIN_EXECUTION_TIME, newTranche} from "@/blockchain/orderlib.js";
|
||||
import RungBuilder from "@/components/chart/RungBuilder.vue";
|
||||
import {computed, ref, watchEffect} from "vue";
|
||||
import {chart, dragging} from "@/charts/chart.js";
|
||||
import AbsoluteTimeEntry from "@/components/AbsoluteTimeEntry.vue";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
const s = useStore()
|
||||
const os = useOrderStore()
|
||||
const co = useChartOrderStore()
|
||||
const prefs = usePrefStore()
|
||||
const props = defineProps(['order', 'builder'])
|
||||
const emit = defineEmits(['update:builder'])
|
||||
|
||||
const minWidth = computed(()=>co.intervalSecs)
|
||||
const stdWidth = computed(()=>10 * minWidth.value)
|
||||
|
||||
function computeDefaultColor() {
|
||||
const index = props.order.builders.indexOf(props.builder)
|
||||
return sideColor(props.order.buy, index)
|
||||
}
|
||||
|
||||
const defaultColor = computeDefaultColor()
|
||||
|
||||
builderDefaults(props.builder, {
|
||||
timeA: s.clock, // todo relative 0
|
||||
timeB: null,
|
||||
// relative: true, // todo
|
||||
relative: false,
|
||||
slippage: DEFAULT_SLIPPAGE,
|
||||
rungs: 1,
|
||||
balance: 0,
|
||||
color: defaultColor,
|
||||
buy: true,
|
||||
})
|
||||
|
||||
const times = ref([])
|
||||
const weights = ref([])
|
||||
|
||||
const amountSymbol = computed(()=>props.order.amountIsTokenA ? co.selectedSymbol.base.s : co.selectedSymbol.quote.s )
|
||||
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(props.order.buy, w, w * props.order.amount, co.selectedSymbol.base.s, amountSymbol.value)))
|
||||
|
||||
const endTimes = computed(()=>{
|
||||
const ts = times.value
|
||||
const window = Math.max(MIN_EXECUTION_TIME, Math.floor((ts[ts.length-1]-ts[0])/props.builder.rungs))
|
||||
return ts.map((t)=>t+window)
|
||||
})
|
||||
const absoluteTimes = computed(()=>{
|
||||
// console.log('absoluteTimes', props.builder.relative, times.value)
|
||||
// if (!props.builder.relative)
|
||||
return times.value
|
||||
// const now = s.clock
|
||||
// return times.value.map((t)=>now+t)
|
||||
})
|
||||
|
||||
const dateStrings = computed(()=>absoluteTimes.value.map((t)=>{
|
||||
const n = DateTime.fromSeconds(t).setZone(prefs.timezone)
|
||||
const y = n.toLocaleString({year:'numeric'})
|
||||
const m = n.toLocaleString({month:'long'})
|
||||
const d = n.toLocaleString({day:'numeric'})
|
||||
const h = n.toLocaleString({hour:'numeric', minute:'numeric'})
|
||||
return `${y} ${m} ${d} ${h}`
|
||||
}))
|
||||
|
||||
watchEffect(()=>{
|
||||
// auto scroll
|
||||
if (!dragging && absoluteTimes.value.length) {
|
||||
const endTime = absoluteTimes.value[absoluteTimes.value.length-1]
|
||||
const range = chart.getVisibleRange()
|
||||
const width = range.to - range.from
|
||||
const now = s.clock
|
||||
const extra = (Math.max(0,endTime - now) + stdWidth.value/2) / width
|
||||
// console.log('visrange', range, width, absV)
|
||||
if (range.to < endTime) {
|
||||
// console.log('scrolling')
|
||||
chart.setVisibleRange({from: now - width, to: now}, {
|
||||
percentRightMargin: Math.round(100 * extra),
|
||||
applyDefaultRightMargin: false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const absTimeA = computed({
|
||||
get() { return _timeEndpoints.value[0] },
|
||||
set(v) {
|
||||
if (v!==null)
|
||||
v = Number(v)
|
||||
updateA(v)
|
||||
}
|
||||
})
|
||||
const absTimeB = computed({
|
||||
get() { return _timeEndpoints.value[1] },
|
||||
set(v) {
|
||||
if (v!==null)
|
||||
v = Number(v)
|
||||
updateB(v)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const _timeEndpoints = ref([props.builder.timeA, props.builder.timeB])
|
||||
const timeEndpoints = computed({
|
||||
get() { return _timeEndpoints.value},
|
||||
set(v) {
|
||||
const [a, b] = v
|
||||
update(a,b, true, true)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function updateA(a) {
|
||||
update(a, _timeEndpoints.value[1], true, false)
|
||||
}
|
||||
|
||||
|
||||
function updateB(b) {
|
||||
update(_timeEndpoints.value[0], b, false, true)
|
||||
}
|
||||
|
||||
|
||||
function update(a, b, updateA, updateB) {
|
||||
if (updateA) {
|
||||
const minB = a + minWidth.value
|
||||
if (b < minB)
|
||||
b = minB
|
||||
}
|
||||
if (updateB) {
|
||||
const maxA = b - minWidth.value
|
||||
if (a > maxA)
|
||||
a = maxA
|
||||
}
|
||||
_timeEndpoints.value = [a, b]
|
||||
props.builder.timeA = a
|
||||
props.builder.timeB = b
|
||||
}
|
||||
|
||||
const flipped = computed(()=>{
|
||||
const a = props.builder.timeA
|
||||
const b = props.builder.timeB
|
||||
return a !== null && b !== null && a > b
|
||||
})
|
||||
|
||||
function buildTranches() {
|
||||
const order = props.order
|
||||
const builder = props.builder
|
||||
const tranches = []
|
||||
const warnings = []
|
||||
|
||||
console.log('buildTranches', builder, order, tranches)
|
||||
const ts = times.value
|
||||
const ets = endTimes.value
|
||||
const ws = weights.value
|
||||
console.log('buildTranches times ends weights', ts, ets, ws)
|
||||
for(let i=0; i<ts.length; i++) {
|
||||
const endTime = Math.max(ets[i],ts[i]+60);
|
||||
console.log('time check', endTime, s.clock)
|
||||
if (endTime <= s.clock)
|
||||
warnings.push(`Tranche already expired at ${new Date(endTime*1000)}`)
|
||||
const t = newTranche({
|
||||
marketOrder: true,
|
||||
slippage: builder.slippage,
|
||||
fraction: ws[i] * MAX_FRACTION,
|
||||
startTime: ts[i],
|
||||
endTime, // always give at least 60 seconds of window to execute
|
||||
})
|
||||
tranches.push(t)
|
||||
}
|
||||
return {tranches, warnings}
|
||||
}
|
||||
|
||||
|
||||
function getModelValue(model) {
|
||||
if(!model) {
|
||||
console.log('getModelValue', model)
|
||||
return null
|
||||
}
|
||||
return model.time
|
||||
}
|
||||
|
||||
function getPointsValue(points) {
|
||||
return points[0].price
|
||||
}
|
||||
|
||||
function setModelValue(model, value) {
|
||||
// console.log('DCA set model value', model, value)
|
||||
// const v = value === null ? null : props.builder.relative ? s.clock + Math.round(value) : Math.round(value)
|
||||
const v = value === null ? null : Math.round(value)
|
||||
if (model.time !== v) {
|
||||
// console.log('DCA do set time', v)
|
||||
model.time = v
|
||||
}
|
||||
}
|
||||
|
||||
function setValues(values) {
|
||||
times.value = values.map((t)=>Math.round(t))
|
||||
}
|
||||
|
||||
function setWeights(ws) { weights.value = ws }
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
td.weight {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:set-values="setLines" :set-weights="setWeights"
|
||||
:set-shapes="setShapes"
|
||||
:std-width="stdWidth" :build-tranches="buildTranches">
|
||||
<table>
|
||||
<table v-if="!co.drawing">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> </td>
|
||||
@@ -32,7 +32,7 @@
|
||||
<v-text-field type="number" v-model="price1A" min="0"
|
||||
density="compact" hide-details variant="outlined"
|
||||
class="mx-1 my-2 price"
|
||||
:color="color" :base-color="color"
|
||||
:color="color"
|
||||
label="Price"
|
||||
/>
|
||||
</td>
|
||||
@@ -49,7 +49,7 @@
|
||||
<v-text-field type="number" v-model="price1B" min="0"
|
||||
density="compact" hide-details variant="outlined"
|
||||
class="mx-1 my-2 price"
|
||||
:color="color" :base-color="color"
|
||||
:color="color"
|
||||
label="Price"
|
||||
/>
|
||||
</td>
|
||||
@@ -68,7 +68,7 @@
|
||||
<v-text-field type="number" v-model="price2A" min="0"
|
||||
density="compact" hide-details variant="outlined"
|
||||
class="mx-1 my-2 price"
|
||||
:color="color" :base-color="color"
|
||||
:color="color"
|
||||
label="Price"
|
||||
/>
|
||||
</td>
|
||||
@@ -82,26 +82,34 @@
|
||||
<v-text-field type="number" v-model="price2B" min="0"
|
||||
density="compact" hide-details variant="outlined"
|
||||
class="mx-1 my-2 price"
|
||||
:color="color" :base-color="color"
|
||||
:color="color"
|
||||
label="Price"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<one-time-hint name="click-chart" activator="#tv-widget" location="center"
|
||||
:when="builder.lineA===null && !co.drew" text="Click the chart!"
|
||||
:on-complete="()=>track('click-chart')"
|
||||
/>
|
||||
</rung-builder>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {applyLinePoints, builderDefaults, useChartOrderStore} from "@/orderbuild.js";
|
||||
import {sideColor} from "@/misc.js";
|
||||
import {sideColor, toPrecisionOrNull} from "@/misc.js";
|
||||
import {MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
|
||||
import RungBuilder from "@/components/chart/RungBuilder.vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {allocationText, DLine} from "@/charts/shape.js";
|
||||
import {vectorEquals, vectorInterpolate} from "@/vector.js";
|
||||
import AbsoluteTimeEntry from "@/components/AbsoluteTimeEntry.vue";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import OneTimeHint from "@/components/OneTimeHint.vue";
|
||||
import {track} from "@/track.js";
|
||||
|
||||
const s = useStore()
|
||||
const co = useChartOrderStore()
|
||||
const props = defineProps(['order', 'builder'])
|
||||
const emit = defineEmits(['update:builder'])
|
||||
@@ -121,9 +129,10 @@ builderDefaults(props.builder, {
|
||||
extendLeft: false,
|
||||
extendRight: true,
|
||||
rungs: 1,
|
||||
skew: 0,
|
||||
balance: 0,
|
||||
breakout: false,
|
||||
color: defaultColor,
|
||||
buy: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -131,6 +140,7 @@ function buildTranches() {
|
||||
const order = props.order
|
||||
const builder = props.builder
|
||||
const tranches = []
|
||||
const warnings = []
|
||||
|
||||
console.log('buildTranches', builder, order, _endpoints.value)
|
||||
const la = _endpoints.value[0] // use the flatline format which is a vector of length 4, useful for vectorInterpolate()
|
||||
@@ -148,6 +158,9 @@ function buildTranches() {
|
||||
t.startTime = reversed ? line[2] : line[0]
|
||||
if (reversed ? !el : !er)
|
||||
t.endTime = reversed ? line[0] : line[2]
|
||||
if (t.endTime <= s.clock)
|
||||
warnings.push(`Tranche already expired at ${new Date(t.endTime*1000)}`)
|
||||
|
||||
// console.log('tranche start/end',
|
||||
// t.startTime === DISTANT_PAST ? 'PAST' : t.startTime,
|
||||
// t.endTime === DISTANT_FUTURE ? 'FUTURE' : t.endTime)
|
||||
@@ -156,7 +169,7 @@ function buildTranches() {
|
||||
}
|
||||
// if( flipped.value )
|
||||
// tranches.reverse()
|
||||
return tranches
|
||||
return {tranches, warnings}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +210,7 @@ const time1A = computed({
|
||||
})
|
||||
|
||||
const price1A = computed({
|
||||
get() { return _endpoints.value[0] === null ? null : _endpoints.value[0][1] },
|
||||
get() { return toPrecisionOrNull(_endpoints.value[0] === null ? null : _endpoints.value[0][1], 6) },
|
||||
set(v) {
|
||||
const flatline0 = _endpoints.value[0];
|
||||
update(
|
||||
@@ -219,7 +232,7 @@ const time1B = computed({
|
||||
})
|
||||
|
||||
const price1B = computed({
|
||||
get() { return _endpoints.value[0] === null ? null : _endpoints.value[0][3] },
|
||||
get() { return toPrecisionOrNull(_endpoints.value[0] === null ? null : _endpoints.value[0][3], 6) },
|
||||
set(v) {
|
||||
const flatline0 = _endpoints.value[0];
|
||||
update(
|
||||
@@ -241,7 +254,7 @@ const time2A = computed({
|
||||
})
|
||||
|
||||
const price2A = computed({
|
||||
get() { return _endpoints.value[1] === null ? null : _endpoints.value[1][1] },
|
||||
get() { return toPrecisionOrNull(_endpoints.value[1] === null ? null : _endpoints.value[1][1], 6) },
|
||||
set(v) {
|
||||
const flatline = _endpoints.value[1];
|
||||
update(
|
||||
@@ -263,7 +276,7 @@ const time2B = computed({
|
||||
})
|
||||
|
||||
const price2B = computed({
|
||||
get() { return _endpoints.value[1] === null ? null : _endpoints.value[1][3] },
|
||||
get() { return toPrecisionOrNull(_endpoints.value[1] === null ? null : _endpoints.value[1][3], 6) },
|
||||
set(v) {
|
||||
const flatline = _endpoints.value[1];
|
||||
update(
|
||||
@@ -276,10 +289,8 @@ const price2B = computed({
|
||||
function update(a, b) { // a and b are lines of two points
|
||||
if (!vectorEquals(props.builder.lineA, a) || !vectorEquals(props.builder.lineB, b)) {
|
||||
_endpoints.value = [flattenLine(a), flattenLine(b)]
|
||||
const newBuilder = {...props.builder}
|
||||
newBuilder.lineA = a
|
||||
newBuilder.lineB = b
|
||||
emit('update:builder', newBuilder)
|
||||
props.builder.lineA = a
|
||||
props.builder.lineB = b
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +359,7 @@ function setWeights(ws) {
|
||||
|
||||
const amountSymbol = computed(()=>props.order.amountIsTokenA ? co.selectedSymbol.base.s : co.selectedSymbol.quote.s )
|
||||
|
||||
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(w, w*props.order.amount, amountSymbol.value)))
|
||||
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(props.order.buy, w, w * props.order.amount, co.selectedSymbol.base.s, amountSymbol.value)))
|
||||
|
||||
const stdWidth = computed(()=>[0, co.meanRange, 0, co.meanRange])
|
||||
|
||||
@@ -398,12 +409,13 @@ function dirtyLine(a, b) {
|
||||
return result
|
||||
}
|
||||
|
||||
const name = computed(()=>props.builder.breakout?'Breakout':'Limit')
|
||||
const name = computed(()=>props.builder.breakout?(props.order.buy?'Breakout':'Breakdown'):'Limit')
|
||||
|
||||
const description = computed(()=>{
|
||||
const buy = props.order.buy
|
||||
const above = buy === props.builder.breakout
|
||||
return (buy?'Buy ':'Sell ')+(above?'above':'below')+' the line'
|
||||
const plural = props.builder.rungs > 1 ? 's' : ''
|
||||
return (buy?'Buy ':'Sell ')+(above?'above':'below')+' the line'+(plural?'s':'')
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
11
src/components/chart/GasMeter.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<rung-builder :name="(builder.breakout?'Breakout':'Limit')"
|
||||
<rung-builder :name="(builder.breakout?(order.buy?'Breakout':'Breakdown'):'Limit')+(builder.rungs>1?' Ladder':'')"
|
||||
:description="description"
|
||||
:order="order" :builder="builder"
|
||||
v-model="priceEndpoints" :mode="0" :flip="flipped"
|
||||
@@ -7,7 +7,7 @@
|
||||
:get-model-value="getModelValue" :set-model-value="setModelValue"
|
||||
:set-values="setPrices" :set-weights="setWeights"
|
||||
:std-width="stdWidth" :build-tranches="buildTranches">
|
||||
<table>
|
||||
<table class="rb">
|
||||
<tbody>
|
||||
<template v-if="prices.length>1">
|
||||
<tr>
|
||||
@@ -15,14 +15,13 @@
|
||||
<v-text-field type="number" v-model="higherPrice" min="0"
|
||||
density="compact" hide-details class="mx-1 my-2" variant="outlined"
|
||||
label="Price"
|
||||
:color="color" :base-color="color"
|
||||
style="flex: 6em"
|
||||
/>
|
||||
</td>
|
||||
<td class="weight">{{ allocationTexts[higherIndex] }}</td>
|
||||
<td class="weight" style="vertical-align: bottom">{{ allocationTexts[higherIndex] }}</td>
|
||||
</tr>
|
||||
<tr v-for="i in innerIndexes" class="ml-5">
|
||||
<td class="pl-5">{{ prices[i] }}</td>
|
||||
<td class="pl-5">{{ toPrecision(prices[i],6) }}</td>
|
||||
<td class="weight">{{ allocationTexts[i] }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -31,14 +30,17 @@
|
||||
<v-text-field type="number" v-model="lowerPrice" min="0"
|
||||
density="compact" hide-details class="mx-1 my-2" variant="outlined"
|
||||
label="Price"
|
||||
:color="color" :base-color="color"
|
||||
style="flex: 6em"
|
||||
/>
|
||||
</td>
|
||||
<td class="weight">{{ weights.length ? allocationTexts[lowerIndex] : '' }}</td>
|
||||
<td class="weight">{{ weights.length > 1 ? allocationTexts[lowerIndex] : '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<one-time-hint name="click-chart" activator="#tv-widget" location="center"
|
||||
:when="priceA===null" text="Click the chart!"
|
||||
:on-complete="()=>track('click-chart')"
|
||||
/>
|
||||
</rung-builder>
|
||||
</template>
|
||||
|
||||
@@ -50,6 +52,9 @@ import {MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
|
||||
import RungBuilder from "@/components/chart/RungBuilder.vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {allocationText, HLine} from "@/charts/shape.js";
|
||||
import OneTimeHint from "@/components/OneTimeHint.vue";
|
||||
import {track} from "@/track.js";
|
||||
import {toPrecision, toPrecisionOrNull} from "@/misc.js";
|
||||
|
||||
const s = useStore()
|
||||
const os = useOrderStore()
|
||||
@@ -71,15 +76,17 @@ builderDefaults(props.builder, {
|
||||
priceA: null,
|
||||
priceB: null,
|
||||
rungs: 1,
|
||||
skew: 0,
|
||||
balance: 0,
|
||||
breakout: false,
|
||||
color: defaultColor,
|
||||
buy: true,
|
||||
})
|
||||
|
||||
function buildTranches() {
|
||||
const order = props.order
|
||||
const builder = props.builder
|
||||
const tranches = []
|
||||
const warnings = []
|
||||
|
||||
console.log('buildTranches', builder, order, tranches)
|
||||
const ps = prices.value
|
||||
@@ -88,17 +95,16 @@ function buildTranches() {
|
||||
let p = ps[i]
|
||||
const w = ws[i]
|
||||
const t = newTranche({
|
||||
// todo start/end
|
||||
fraction: w * MAX_FRACTION,
|
||||
})
|
||||
const symbol = co.selectedSymbol
|
||||
console.log('symbol', symbol, p)
|
||||
// console.log('symbol', symbol, p)
|
||||
applyLinePoint(t, symbol, order.buy, p, builder.breakout)
|
||||
tranches.push(t)
|
||||
}
|
||||
if (!flipped.value)
|
||||
tranches.reverse()
|
||||
return tranches
|
||||
return {tranches, warnings}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,10 +139,8 @@ const priceEndpoints = computed({
|
||||
|
||||
function update(a, b) {
|
||||
_priceEndpoints.value = [a, b]
|
||||
const newBuilder = {...props.builder}
|
||||
newBuilder.priceA = a
|
||||
newBuilder.priceB = b
|
||||
emit('update:builder', newBuilder)
|
||||
props.builder.priceA = a
|
||||
props.builder.priceB = b
|
||||
}
|
||||
|
||||
const flipped = computed(()=>{
|
||||
@@ -146,7 +150,7 @@ const flipped = computed(()=>{
|
||||
})
|
||||
|
||||
const higherPrice = computed({
|
||||
get() { return flipped.value ? priceA.value : priceB.value },
|
||||
get() { return toPrecisionOrNull(flipped.value ? priceA.value : priceB.value, 6) },
|
||||
set(v) {
|
||||
if (flipped.value)
|
||||
priceA.value = v
|
||||
@@ -167,9 +171,7 @@ const innerIndexes = computed(()=>{
|
||||
})
|
||||
|
||||
const lowerPrice = computed({
|
||||
get() {
|
||||
return !flipped.value ? priceA.value : priceB.value
|
||||
},
|
||||
get() {return toPrecisionOrNull(!flipped.value ? priceA.value : priceB.value, 6)},
|
||||
set(v) {
|
||||
if (!flipped.value)
|
||||
priceA.value = v
|
||||
@@ -195,13 +197,14 @@ function setPrices(ps) {prices.value = ps}
|
||||
function setWeights(ws) { weights.value = ws }
|
||||
|
||||
const amountSymbol = computed(()=>props.order.amountIsTokenA ? co.selectedSymbol.base.s : co.selectedSymbol.quote.s )
|
||||
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(w, w*props.order.amount, amountSymbol.value)))
|
||||
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(props.order.buy, w, w * props.order.amount, co.selectedSymbol.base.s, amountSymbol.value)))
|
||||
const color = computed(()=>props.builder.color ? props.builder.color : defaultColor)
|
||||
const stdWidth = computed(()=>co.meanRange)
|
||||
const description = computed(()=>{
|
||||
const buy = props.order.buy
|
||||
const above = buy === props.builder.breakout
|
||||
return (buy?'Buy ':'Sell ')+(above?'above':'below')+' the line'
|
||||
const plural = props.builder.rungs > 1 ? 's' : ''
|
||||
return (buy?'Buy ':'Sell ')+(above?'above':'below')+' the line'+plural
|
||||
})
|
||||
|
||||
function getModelValue(model) {
|
||||
@@ -227,5 +230,16 @@ td.weight {
|
||||
padding-right: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
table.rb {
|
||||
padding: 0;
|
||||
border-spacing: 0;
|
||||
tbody {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {builderDefaults, builderFuncs, DEFAULT_SLIPPAGE, useChartOrderStore} from "@/orderbuild.js";
|
||||
import {builderDefaults, builderFuncs, useChartOrderStore} from "@/orderbuild.js";
|
||||
import {computed, onMounted, onUnmounted} from "vue";
|
||||
import {newTranche} from "@/blockchain/orderlib.js";
|
||||
import {DEFAULT_SLIPPAGE, newTranche} from "@/blockchain/orderlib.js";
|
||||
|
||||
const co = useChartOrderStore()
|
||||
const props = defineProps(['order', 'builder'])
|
||||
@@ -36,8 +36,17 @@ const slippage = computed({
|
||||
set(v) {props.builder.slippage=v/100; emit('update:builder', props.builder)}
|
||||
})
|
||||
|
||||
const MIN_SLIPPAGE = 0.01;
|
||||
|
||||
function buildTranches() {
|
||||
return [newTranche({slippage: slippage.value/100})]
|
||||
let warnings = []
|
||||
if (slippage.value < MIN_SLIPPAGE)
|
||||
warnings.push('Slippage will be set to the minimum of 0.01%')
|
||||
const slip = Math.max(slippage.value, MIN_SLIPPAGE)
|
||||
return {
|
||||
tranches: [newTranche({marketOrder: true, slippage: slip / 100})],
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => builderFuncs[props.builder.id] = buildTranches)
|
||||
|
||||
135
src/components/chart/OrderAmount.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="d-flex flex-row align-start">
|
||||
<!--
|
||||
<v-btn v-if="!multiplier"
|
||||
variant="outlined" :color="color"
|
||||
:text="(props.order.buy ? 'Buy ' : 'Sell ') + co.selectedSymbol.base.s"
|
||||
@click="props.order.buy=!props.order.buy"
|
||||
class="ml-3 mt-2 mr-2"
|
||||
size="2.5em"
|
||||
style="width: 9em"
|
||||
/>
|
||||
-->
|
||||
<div class="d-flex flex-column align-center">
|
||||
<v-switch v-if="!multiplier" v-model="switchModel" :color="sideColor" :base-color="sideColor" density="compact"
|
||||
class="my-0 mx-3 clickable">
|
||||
<template #prepend>
|
||||
<span :style="order.buy?{color:theme.colors.success}:{}" @click="order.buy=true" class="bs-button buy">Buy<br/>{{co.selectedSymbol.base.s}}</span>
|
||||
</template>
|
||||
<template #append>
|
||||
<span :style="!order.buy?{color:theme.colors.error}:{}" @click="order.buy=false" class="bs-button sell">Sell<br/>{{co.selectedSymbol.base.s}}</span>
|
||||
</template>
|
||||
</v-switch>
|
||||
</div>
|
||||
<v-text-field type="number" inputmode="numeric" pattern="[0-9]*\.?[0-9]*" v-model="amount" variant="outlined"
|
||||
:hint="available" :persistent-hint="true"
|
||||
min="0"
|
||||
class="amount mx-3"
|
||||
style="max-width: 20em"
|
||||
:color="color"
|
||||
:label="order.amountIsTokenA ?
|
||||
(multiplier ? 'Amount each' : 'Amount') :
|
||||
((multiplier ? 'Value each in ' : 'Value in ')+co.selectedSymbol.quote.s)">
|
||||
<template #prepend-inner>
|
||||
<v-btn variant="text" text="max" class="px-0" size="small"
|
||||
:disabled="!maxAmount || order.amountIsTokenA===order.buy && !co.price" @click="setMax"/>
|
||||
</template>
|
||||
<template #append-inner>
|
||||
<v-btn :text="order.amountIsTokenA?co.baseToken.s:co.quoteToken.s+' worth'"
|
||||
variant="text" @click="toggleAmountToken" style="width: 7em"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import {computed, ref} from "vue";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import Color from "color";
|
||||
import {useTheme} from "vuetify";
|
||||
|
||||
const props = defineProps(['order', 'multiplier', 'color'])
|
||||
|
||||
const s = useStore()
|
||||
const co = useChartOrderStore()
|
||||
|
||||
const theme = useTheme().current
|
||||
// console.log('theme', theme.value)
|
||||
const sideColor = computed(()=>new Color(props.order.buy?theme.value.colors.success:theme.value.colors.error).darken(0.2).string())
|
||||
const switchModel = computed({
|
||||
get() {return !props.order.buy},
|
||||
set(v) {props.order.buy=!v}
|
||||
})
|
||||
|
||||
const amount = computed({
|
||||
get() {
|
||||
let result = props.order.amount
|
||||
if (result !== null && props.multiplier)
|
||||
result /= props.multiplier
|
||||
return result
|
||||
},
|
||||
set(v) {
|
||||
if (v !== null && props.multiplier)
|
||||
v *= props.multiplier
|
||||
props.order.amount = v
|
||||
}
|
||||
})
|
||||
|
||||
const available = computed(()=>{
|
||||
const max = maxAmount.value
|
||||
return max === null ? '' : `Available: ${maxAmount.value} ${inToken.value.s}`
|
||||
})
|
||||
|
||||
const inToken = computed( ()=>props.order.buy ? co.quoteToken : co.baseToken )
|
||||
|
||||
const maxAmount = computed(()=>{
|
||||
const token = inToken.value;
|
||||
if (!token)
|
||||
return null
|
||||
const balance = s.balances[token.a]
|
||||
if( !balance )
|
||||
return null
|
||||
return balance / 10**token.d
|
||||
})
|
||||
|
||||
const lastMaxValue = ref(-1)
|
||||
|
||||
function setMax() {
|
||||
let amount = maxAmount.value
|
||||
if (amount) {
|
||||
const order = props.order
|
||||
const price = co.price
|
||||
if (order.amountIsTokenA===order.buy) {
|
||||
if (order.buy)
|
||||
amount /= price
|
||||
else
|
||||
amount *= price
|
||||
}
|
||||
amount = Number(amount.toPrecision(7))
|
||||
lastMaxValue.value = amount
|
||||
order.amount = amount
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAmountToken() {
|
||||
const order = props.order
|
||||
order.amountIsTokenA=!order.amountIsTokenA
|
||||
if (order.amount === lastMaxValue.value)
|
||||
setMax()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
.amount {
|
||||
max-width: 30em;
|
||||
}
|
||||
.bs-button {
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
&.buy:hover {color: $green;}
|
||||
&.sell:hover {color: $red;}
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +1,50 @@
|
||||
<template>
|
||||
<builder-panel :order="order" :builder="builder" :build-tranches="buildTranches"
|
||||
:adjust-shapes="adjustShapes" :delete-shapes="deleteShapes">
|
||||
:adjust-shapes="adjustShapes" :delete-shapes="deleteShapes" :name="name">
|
||||
<div style="min-width: 4em; font-size: larger" :style="colorStyle"
|
||||
class="d-flex flex-column align-self-start ml-2">
|
||||
class="d-flex flex-column">
|
||||
<!--
|
||||
<div class="flex-row align-items-center">
|
||||
<v-btn variant="outlined" style="width: 8em"
|
||||
@click="props.builder.breakout=!props.builder.breakout">{{ name }}</v-btn>
|
||||
@click="()=>{if (props.builder.breakout!==undefined) props.builder.breakout=!props.builder.breakout}">{{ name }}</v-btn>
|
||||
<div class="description w-100 text-center">{{description}}</div>
|
||||
</div>
|
||||
-->
|
||||
<!--
|
||||
<floating-div id="rungs" v-if="endpoints[0]">
|
||||
-->
|
||||
<v-text-field type="number" v-model="rungs"
|
||||
density="compact" hide-details class="mx-1 my-2" variant="outlined"
|
||||
label="Rungs"
|
||||
:color="color" :base-color="color" min="1" :max="MAX_RUNGS"
|
||||
:color="color"
|
||||
min="1" :max="MAX_RUNGS"
|
||||
:disabled="rungsDisabled"
|
||||
style="width: 6.6em;"
|
||||
id="rungs"
|
||||
/>
|
||||
<!--
|
||||
</floating-div>
|
||||
-->
|
||||
<one-time-hint name="rungs" activator="#rungs" after="choose-builder"
|
||||
text="↓ Try increasing rungs!" location="top"
|
||||
:when="rungs===1&&endpoints[0]!==null"
|
||||
:on-complete="()=>track('rungs')"
|
||||
/>
|
||||
<v-tooltip v-if="builder.breakout!==undefined"
|
||||
:text="order.buy?'Breakout orders buy above the breakout line':'Breakdown orders sell below the breakdown line'">
|
||||
<template #activator="{ props }">
|
||||
<div v-bind="props">
|
||||
<v-switch v-model="breakout" :label="order.buy?'Breakout':'Breakdown'"
|
||||
persistent-hint :color="switchColor" :base-color="switchColor" hide-details direction="vertical"
|
||||
density="compact"/>
|
||||
<div class="mx-auto">
|
||||
<span style="font-size: .7em; vertical-align: top"
|
||||
:style="builder.breakout?{color:new Color(color).lighten(0.5).string()}:null">
|
||||
{{ description }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<slot/>
|
||||
@@ -23,17 +53,32 @@
|
||||
<v-icon icon="mdi-chat-alert-outline" color="grey" class="mr-1"/>
|
||||
Click the chart!
|
||||
</div>
|
||||
<div v-if="rungs>1" class="mx-2 d-flex align-center">
|
||||
<v-slider v-if="rungs>1" :direction="orientation?'vertical':'horizontal'" min="-100" max="100" v-model="skew100"
|
||||
<div v-if="rungs>1" class="mx-2 d-flex justify-start">
|
||||
<div class="d-flex align-center mt-2">
|
||||
<!--
|
||||
<floating-div id="slider" v-if="endpoints[0]">
|
||||
-->
|
||||
<div id="balance-slider">
|
||||
<v-slider v-if="rungs>1" :direction="orientation?'vertical':'horizontal'" min="-100" max="100" v-model="balance100"
|
||||
class="no-slider-bg ml-2 mr-4" hide-details/>
|
||||
<v-text-field type="number" v-model="skew100" min="-100" max="100"
|
||||
density="compact" hide-details variant="outlined" label="Skew" step="5"
|
||||
:color="color" :base-color="color" class="skew">
|
||||
</div>
|
||||
<!--
|
||||
</floating-div>
|
||||
-->
|
||||
<one-time-hint name="balance-slider" activator="#balance-slider" after="rungs"
|
||||
text="↓ Slide the amount balance ↓" location="top"
|
||||
:when="balance100===0"
|
||||
:on-complete="()=>track('balance-slider')"
|
||||
/>
|
||||
<v-text-field type="number" v-model="balance100" min="-100" max="100"
|
||||
density="compact" hide-details variant="outlined" label="Balance" step="5"
|
||||
class="balance">
|
||||
<template v-slot:prepend>
|
||||
<v-btn icon="mdi-scale-balance" variant="plain" @click="builder.skew=0" :color="color"/>
|
||||
<v-btn icon="mdi-scale-balance" variant="plain" @click="builder.balance=0"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
</div>
|
||||
</builder-panel>
|
||||
</template>
|
||||
|
||||
@@ -56,6 +101,10 @@ import {
|
||||
vectorNeg,
|
||||
vectorSub
|
||||
} from "@/vector.js";
|
||||
import {logicalXOR} from "@/common.js";
|
||||
import OneTimeHint from "@/components/OneTimeHint.vue";
|
||||
import {track} from "@/track.js";
|
||||
import FloatingDiv from "@/components/FloatingDiv.vue";
|
||||
|
||||
const co = useChartOrderStore()
|
||||
const endpoints = defineModel('modelValue') // 2-item list of points/values
|
||||
@@ -68,7 +117,7 @@ const props = defineProps({
|
||||
stdWidth: [Number, Array],
|
||||
shape: Function, // shape() -> Shape
|
||||
mode: { type: Number, default: 0 }, // rung addition mode: 0 = split, 1 = extend
|
||||
flip: { type: Boolean, default: false }, // if true, the skew slider is flipped upside-down
|
||||
flip: { type: Boolean, default: false }, // if true, the balance slider is flipped upside-down
|
||||
orientation: { type: Number, default: 1 }, // 0 = horizontal slider, 1 = vertical
|
||||
// values may be scalars or vector arrays
|
||||
getModelValue: Function, // getModelValue(model) -> value
|
||||
@@ -80,16 +129,18 @@ const props = defineProps({
|
||||
|
||||
const flippedSign = computed(()=>props.flip?-1:1)
|
||||
|
||||
const skew100 = computed( {
|
||||
get() {return flippedSign.value*props.builder.skew*100},
|
||||
set(v) {props.builder.skew = flippedSign.value*v/100; }
|
||||
const balance100 = computed( {
|
||||
get() {return flippedSign.value*props.builder.balance*100},
|
||||
set(v) {
|
||||
// if (v<-60) v = -60;
|
||||
props.builder.balance = flippedSign.value*v/100; }
|
||||
} )
|
||||
|
||||
// validity checks
|
||||
watchEffect(()=>{
|
||||
const rungs = props.builder.rungs
|
||||
// const prev = props.builder.valid
|
||||
props.builder.valid = rungs >= 1 && endpoints.value[0] && (rungs < 2 || endpoints.value[1])
|
||||
props.builder.valid = rungs >= 1 && endpoints.value[0] > 0 && (rungs < 2 || endpoints.value[1])
|
||||
// console.log('valid?', prev, props.builder.valid, rungs, valueA.value, valueB.value)
|
||||
})
|
||||
|
||||
@@ -101,8 +152,12 @@ watchEffect(()=>{
|
||||
}
|
||||
})
|
||||
|
||||
const breakout = computed({
|
||||
get() {return !logicalXOR(props.builder.breakout, props.order.buy)},
|
||||
set(v) {props.builder.breakout = !logicalXOR(v, props.order.buy)},
|
||||
})
|
||||
|
||||
function setEndpoints(a, b) {
|
||||
// console.log('rb setting endpoints', devectorize(a), devectorize(b))
|
||||
endpoints.value = [devectorize(a), devectorize(b)]
|
||||
}
|
||||
|
||||
@@ -125,8 +180,7 @@ const rungs = computed({
|
||||
r = Number(r)
|
||||
const prevR = Number(props.builder.rungs)
|
||||
props.builder.rungs = r
|
||||
// console.log('set rungs', prevR, r, a, b)
|
||||
if ( r > 0 && vectorIsNull(b) ) {
|
||||
if ( prevR === 1 && r > 1 ) {
|
||||
// convert single shape to a range
|
||||
if (props.mode===0) {
|
||||
const width = vectorize(props.stdWidth)
|
||||
@@ -143,7 +197,7 @@ const rungs = computed({
|
||||
else
|
||||
throw Error(`Unknown rung mode ${props.mode}`)
|
||||
}
|
||||
else if ( r === 1 && !vectorIsNull(b) ) {
|
||||
else if ( prevR > 1 && r === 1 ) {
|
||||
// convert from a range to a single shape
|
||||
if (props.mode===0)
|
||||
a = vectorDiv(vectorAdd(a,b), 2)
|
||||
@@ -194,14 +248,10 @@ const values = computed(()=>{
|
||||
|
||||
|
||||
const weights = computed(() => {
|
||||
// const skew = props.flip ? -props.builder.skew : props.builder.skew
|
||||
// const balance = props.flip ? -props.builder.balance : props.builder.balance
|
||||
const most = 0.998
|
||||
let skew = -props.builder.skew
|
||||
if (skew <= -1)
|
||||
skew = -most
|
||||
else if (skew >= 1)
|
||||
skew = most
|
||||
const ws = linearWeights(props.builder.rungs, skew)
|
||||
let balance = Math.min(most, Math.max(-most, -props.builder.balance))
|
||||
const ws = linearWeights(props.builder.rungs, balance)
|
||||
if (props.setWeights)
|
||||
props.setWeights(ws)
|
||||
return ws
|
||||
@@ -227,8 +277,10 @@ const color = computed({
|
||||
props.builder.color = c.saturation <= maxLightness ? v : c.lightness(maxLightness).rgb().string()
|
||||
}
|
||||
})
|
||||
const switchColor = computed(()=>props.builder.breakout ? color.value : null)
|
||||
const colorStyle = computed(() => {
|
||||
return {'color': color.value}
|
||||
// return {'color': color.value}
|
||||
return {}
|
||||
})
|
||||
|
||||
|
||||
@@ -261,9 +313,9 @@ function translateOnModel(shape) {
|
||||
if (!this.beingDragged())
|
||||
return
|
||||
const prev = getModelValue(oldModel)
|
||||
const cur = vectorize(getModelValue(this.model))
|
||||
const cur = vectorize(getModelValue(model))
|
||||
const delta = vectorSub(cur, prev)
|
||||
// console.log('delta', shape.id, prev, cur, delta)
|
||||
// console.log('translateOnModel delta', shape.id, prev, cur, delta)
|
||||
let [a, b] = endpoints.value
|
||||
a = vectorize(a)
|
||||
if (!vectorIsZero(delta)) {
|
||||
@@ -330,10 +382,12 @@ function makeModel(index) {
|
||||
allocation: alloc,
|
||||
maxAllocation: Math.max(...weights.value),
|
||||
amount: props.order.amount * alloc,
|
||||
baseSymbol: co.selectedSymbol.base.s,
|
||||
amountSymbol: amountSymbol.value,
|
||||
textLocation: above ? 'above' : 'below',
|
||||
breakout: props.builder.breakout,
|
||||
extraText: null,
|
||||
buy,
|
||||
}
|
||||
setModelValue(result, values.value[index])
|
||||
return result
|
||||
@@ -413,6 +467,8 @@ function deleteShapes() {
|
||||
|
||||
if (!endpoints.value[0])
|
||||
shapeA.createOrDraw(); // initiate drawing mode
|
||||
else
|
||||
adjustShapes()
|
||||
|
||||
</script>
|
||||
|
||||
@@ -426,7 +482,7 @@ if (!endpoints.value[0])
|
||||
:deep(.v-slider.no-slider-bg .v-slider-track__fill) {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
.skew {
|
||||
.balance {
|
||||
min-width: 9em;
|
||||
max-width: 12em;
|
||||
}
|
||||
|
||||
28
src/components/chart/Shared.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<h2>Shared Order</h2>
|
||||
<p>Loading the shared order into the app...</p>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {loadShareUrl} from "@/share.js";
|
||||
import {router} from "@/router/router.js";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const route = useRoute()
|
||||
const code = route.params.code
|
||||
loadShareUrl(code).then((ok)=> {
|
||||
if (ok) {
|
||||
console.log('loaded share data',code)
|
||||
router.replace('/order');
|
||||
}
|
||||
else {
|
||||
console.log('failed to load share data',code)
|
||||
}
|
||||
}).catch((e)=> {
|
||||
console.error(e)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="d-flex mb-1 align-center w-100">
|
||||
<logo class="d-flex align-end clickable logo-large ml-1" @click="nav('Order')" :show-tag="true" max-height="32"/>
|
||||
<logo class="d-flex align-end clickable logo-large ml-1" @click="// noinspection JSIgnoredPromiseFromCall
|
||||
router.push({name: 'Order'})" :show-tag="true" max-height="32"/>
|
||||
<slot/>
|
||||
<div class="ml-auto d-flex align-center">
|
||||
<span class="title mr-4">{{title}}</span>
|
||||
@@ -9,7 +10,7 @@
|
||||
<toolbar-button tooltip="Assets" icon="mdi-currency-btc" route="Assets"/>
|
||||
<!-- mdi-format-list-checks mdi-format-list-bulleted-square -->
|
||||
<toolbar-button tooltip="Status" icon="mdi-format-list-checks" route="Status"/>
|
||||
<toolbar-button tooltip="About" icon="mdi-information-outline" href="https://dexorder.trade/" target="dexorder"/>
|
||||
<toolbar-button tooltip="About" icon="mdi-information-outline" href="https://dexorder.com/" target="dexorderwww"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,7 +18,7 @@
|
||||
<script setup>
|
||||
import ToolbarButton from "@/components/chart/ToolbarButton.vue";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
import {nav} from "@/misc.js";
|
||||
import {router} from "@/router/router.js";
|
||||
|
||||
const props = defineProps(['title', 'icon'])
|
||||
|
||||
|
||||
@@ -9,11 +9,9 @@
|
||||
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {nav} from "/src/misc.js"
|
||||
import {router} from "@/router/router.js";
|
||||
|
||||
const props = defineProps(['icon', 'route', 'tooltip', 'href', 'target'])
|
||||
const router = useRoute();
|
||||
const isCurrent = computed(() => router.name === props.route)
|
||||
|
||||
function click() {
|
||||
@@ -29,7 +27,8 @@ function click() {
|
||||
|
||||
}
|
||||
else
|
||||
nav(props.route)
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
router.push({name: props.route})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<script setup>
|
||||
function openApp() {
|
||||
window.open('https://app.dexorder.trade/', 'dexorderapp')
|
||||
window.open('https://app.dexorder.com/', 'dexorderapp')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
</v-card-text>
|
||||
<div class="w-100 d-flex justify-center my-6 actions">
|
||||
<app-btn/>
|
||||
<v-btn prepend-icon="mdi-information-outline" variant="flat" text="How It Works" @click="nav('HowItWorks')"/>
|
||||
<v-btn prepend-icon="mdi-information-outline" variant="flat" text="How It Works" @click="// noinspection JSIgnoredPromiseFromCall
|
||||
router.push({name: 'HowItWorks'})"/>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -36,10 +37,8 @@ import beta from "@/components/Beta.vue";
|
||||
import Soon from "@/components/Soon.vue";
|
||||
import UniswapLogo from "@/corp/UniswapLogo.vue";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
import {nav} from "@/misc.js";
|
||||
import AppBtn from "@/corp/AppBtn.vue";
|
||||
import Social from "@/components/Social.vue";
|
||||
|
||||
import {router} from "@/router/router.js";
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center my-6 actions">
|
||||
<app-btn/>
|
||||
<v-btn prepend-icon="mdi-home-outline" variant="flat" text="Home" @click="nav('Home')"/>
|
||||
<v-btn prepend-icon="mdi-home-outline" variant="flat" text="Home" @click="// noinspection JSIgnoredPromiseFromCall
|
||||
router.push({name: 'Home'})"/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -57,8 +58,8 @@
|
||||
<script setup>
|
||||
|
||||
import UniswapLogo from "@/corp/UniswapLogo.vue";
|
||||
import {nav} from "@/misc.js";
|
||||
import AppBtn from "@/corp/AppBtn.vue";
|
||||
import {router} from "@/router/router.js";
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
<!-- <v-app-bar-nav-icon @click="s.nav=!s.nav" icon="mdi-plus"/>-->
|
||||
|
||||
<v-app-bar-title>
|
||||
<logo @click="nav('Home')" class="clickable"/>
|
||||
<logo @click="// noinspection JSIgnoredPromiseFromCall
|
||||
router.push({name: 'Home'})" class="clickable"/>
|
||||
<social class="d-inline" size="small"/>
|
||||
</v-app-bar-title>
|
||||
|
||||
<v-btn @click="nav('HowItWorks')" prepend-icon="mdi-information-outline" text="How It Works"/>
|
||||
<v-btn @click="// noinspection JSIgnoredPromiseFromCall
|
||||
router.push({name: 'HowItWorks'})" prepend-icon="mdi-information-outline" text="How It Works"/>
|
||||
<v-btn prepend-icon="mdi-arrow-up-bold" variant="tonal" color="primary" @click="openApp" text="Launch App"/>
|
||||
|
||||
</v-app-bar>
|
||||
@@ -16,13 +18,13 @@
|
||||
<script setup>
|
||||
import {useTheme} from "vuetify";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
import {nav} from "@/misc.js";
|
||||
import Social from "@/components/Social.vue";
|
||||
import {router} from "@/router/router.js";
|
||||
|
||||
const theme = useTheme().current
|
||||
|
||||
function openApp() {
|
||||
window.open('https://app.dexorder.trade/', 'dexorderapp')
|
||||
window.open('https://app.dexorder.com/', 'dexorderapp')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
47
src/debug_console.js
Normal file
@@ -0,0 +1,47 @@
|
||||
(function() {
|
||||
let debugDiv = document.createElement('div');
|
||||
debugDiv.id = 'debug-log';
|
||||
debugDiv.setAttribute(
|
||||
'style',
|
||||
`
|
||||
background:#222 !important;
|
||||
color:#0f0 !important;
|
||||
padding:8px !important;
|
||||
font-family:monospace !important;
|
||||
font-size:14px !important;
|
||||
position:fixed !important;
|
||||
left:0 !important; right:0 !important;
|
||||
bottom:0 !important;
|
||||
max-height:35vh !important;
|
||||
width:100vw !important;
|
||||
overflow-y:auto !important;
|
||||
z-index:9999 !important;
|
||||
pointer-events:auto !important;
|
||||
box-shadow:0 0 8px #000 !important;
|
||||
touch-action: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-word !important;
|
||||
`
|
||||
);
|
||||
document.body.appendChild(debugDiv);
|
||||
|
||||
function printLog(type, args) {
|
||||
let msg = Array.from(args).map(a => {
|
||||
try { return typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a); }
|
||||
catch { return String(a); }
|
||||
}).join(' ');
|
||||
let line = document.createElement('div');
|
||||
line.textContent = `[${type}] ${msg}`;
|
||||
debugDiv.appendChild(line);
|
||||
debugDiv.scrollTop = debugDiv.scrollHeight;
|
||||
}
|
||||
|
||||
['trace', 'log', 'info', 'warn', 'error'].forEach(type => {
|
||||
let orig = console[type];
|
||||
console[type] = function(...args) {
|
||||
printLog(type, args);
|
||||
orig.apply(console, args);
|
||||
}
|
||||
});
|
||||
})();
|
||||
88
src/fees.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import {newContract, vaultContract} from "@/blockchain/contract.js";
|
||||
import {timestamp} from "@/common.js";
|
||||
import TTLCache from "@isaacs/ttlcache";
|
||||
import {provider} from "@/blockchain/provider.js";
|
||||
|
||||
|
||||
async function getFeeManagerContract(vaultContract) {
|
||||
const feeManagerAddr = await vaultContract.feeManager()
|
||||
return await newContract(feeManagerAddr, 'IFeeManager', provider);
|
||||
}
|
||||
|
||||
|
||||
export async function getFeeSchedule(vaultAddr) {
|
||||
if (feeSchedCache.has(vaultAddr))
|
||||
return feeSchedCache.get(vaultAddr)
|
||||
const vault = await vaultContract(vaultAddr, provider)
|
||||
const feeManager = await getFeeManagerContract(vault);
|
||||
const [sched, changeTimestamp] = await Promise.all([feeManager.fees(), feeManager.proposedFeeActivationTime()])
|
||||
const changing = Number(changeTimestamp)
|
||||
const newSched = !changing ? null : await feeManager.proposedFees()
|
||||
// if it's not changing, we have an hour (wait 55 minutes) until another fee change could happen
|
||||
// otherwise, set the TTL to be a long TTL after the changeover
|
||||
const noticePeriod = 55*60
|
||||
const ttl = (!changing ? noticePeriod : (changing - timestamp() + noticePeriod))*1000 // milliseconds
|
||||
const schedule = new FeeSchedule(sched, newSched);
|
||||
feeSchedCache.set(vaultAddr, schedule, {ttl})
|
||||
return schedule
|
||||
}
|
||||
|
||||
|
||||
export async function placementFee(vaultContractOrAddr, order, window = 300) {
|
||||
// If the fees are about to change within `window` seconds of now, we send the higher native amount of the two fees.
|
||||
// If the fees sent are too much, the vault will refund the sender.
|
||||
const vault = typeof vaultContractOrAddr === 'string' ? await vaultContract(vaultContractOrAddr, provider) : vaultContractOrAddr
|
||||
const feeManager = await getFeeManagerContract(vault);
|
||||
const [sched, changeTimestamp] = await Promise.all([feeManager.fees(), feeManager.proposedFeeActivationTime()])
|
||||
console.log('sched', order, sched)
|
||||
// single order placement selector
|
||||
const placementFeeSelector = 'placementFee((address,address,(uint8,uint24),uint256,uint256,bool,bool,bool,uint64,(uint16,bool,bool,bool,bool,bool,bool,bool,bool,uint16,uint24,uint32,uint32,(uint32,uint32),(uint32,uint32))[]),(uint8,uint8,uint8,uint8,uint8))'
|
||||
let [orderFee, gasFee] = await vault[placementFeeSelector](order, [...sched])
|
||||
console.log('placementFee', orderFee, gasFee)
|
||||
if (Number(changeTimestamp) - timestamp() < window) {
|
||||
const nextSched = await feeManager.proposedFees()
|
||||
console.log('nextSched', new Date(Number(changeTimestamp)*1000), nextSched)
|
||||
const [nextOrderFee, nextGasFee] = await vault[placementFeeSelector](order, [...nextSched])
|
||||
if (nextOrderFee + nextGasFee > orderFee + gasFee)
|
||||
[orderFee, gasFee] = [nextOrderFee, nextGasFee]
|
||||
}
|
||||
return [orderFee, gasFee]
|
||||
}
|
||||
|
||||
|
||||
function schedToDict(sched) {
|
||||
if (sched===null)
|
||||
return null
|
||||
const [ofee, oexp, gfee, gexp, ffee] = sched
|
||||
console.log('sched', ofee, oexp, gfee, gexp, ffee)
|
||||
return {
|
||||
orderFee: BigInt(ofee) << BigInt(oexp), // in wei
|
||||
gasFee: BigInt(gfee) << BigInt(gexp), // in wei
|
||||
fillFee: Number(ffee) / 200, // float coefficient
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class FeeSchedule {
|
||||
constructor(sched, nextSched=null) {
|
||||
// if nextSched is set, the more expensive of the two fees will be returned
|
||||
this.sched = schedToDict(sched)
|
||||
this.nextSched = schedToDict(nextSched)
|
||||
|
||||
const _max = (method, ...args) => {
|
||||
const curVal = this[method](this.sched,...args);
|
||||
return this.nextSched === null ? curVal : Math.max(curVal, this[method](this.nextSched,...args))
|
||||
}
|
||||
|
||||
this.gasFee = _max( '_gasFee')
|
||||
this.orderFee = _max('_orderFee')
|
||||
this.fillFee = _max('_fillFee')
|
||||
}
|
||||
|
||||
_gasFee(sched) {return sched.gasFee}
|
||||
_orderFee(sched) {return sched.orderFee}
|
||||
_fillFee(sched) {return sched.fillFee}
|
||||
|
||||
}
|
||||
|
||||
const feeSchedCache = new TTLCache()
|
||||
@@ -4,9 +4,13 @@
|
||||
<v-dialog v-model="showTransactionDialog" max-width="300">
|
||||
<v-card :title="title">
|
||||
<v-card-text v-if="description!==null">{{description}}</v-card-text>
|
||||
<v-card-text>Confirm this {{noun}} in your wallet.</v-card-text>
|
||||
<v-card-text v-if="!s.vault">Creating your trading vault smart contract. Please wait a few seconds...</v-card-text>
|
||||
<v-card-text v-if="s.vault && s.creatingVault">Verifying your trading vault...</v-card-text>
|
||||
<v-card-text v-if="s.vault && !s.creatingVault && ws.transaction !== null && ws.transaction.state === TransactionState.Proposed">Confirm this {{noun}} in your wallet.</v-card-text>
|
||||
<v-card-text v-if="s.vault && !s.creatingVault && ws.transaction !== null && ws.transaction.state !== TransactionState.Proposed">Signed and sent! Waiting for blockchain confirmation...</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<pool-selection-dialog/>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
@@ -15,8 +19,9 @@ import MainView from './MainView.vue'
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {computed} from "vue";
|
||||
import {useWalletStore} from "@/blockchain/wallet.js";
|
||||
import {TransactionType} from "@/blockchain/transaction.js";
|
||||
import {FixedNumber} from "ethers";
|
||||
import PoolSelectionDialog from "@/components/PoolSelectionDialog.vue";
|
||||
import {TransactionState, TransactionType} from "@/blockchain/transactionDecl.js";
|
||||
|
||||
const s = useStore()
|
||||
const ws = useWalletStore()
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<v-icon icon="mdi-arrow-up-bold" size="x-small" class="arrow" color="green"/>
|
||||
<span class="clickable">dexorder</span>
|
||||
</span>
|
||||
<v-chip text="BETA" size="x-small" color="red" class="mx-1"/>
|
||||
</v-app-bar-title>
|
||||
|
||||
<v-btn icon="mdi-safe-square" color="grey-darken-2" text="Vault" @click="route('Assets')"></v-btn>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { createApp } from 'vue'
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
import '@/styles/style.scss'
|
||||
import "./socket.js"
|
||||
import "./socketInit.js"
|
||||
import "./version.js"
|
||||
|
||||
BigInt.prototype.toJSON = function() { return this.toString() }
|
||||
|
||||
71
src/misc.js
@@ -3,13 +3,8 @@ import {usePrefStore, useStore} from "@/store/store.js";
|
||||
import {token} from "@/blockchain/token.js";
|
||||
import Color from "color";
|
||||
import {DateTime} from "luxon";
|
||||
import router from "@/router/index.js";
|
||||
import {dateString} from "@/common.js";
|
||||
|
||||
export function nav(name) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
router.push({name})
|
||||
}
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const QUOTE_SYMBOLS = [
|
||||
// in order of preference
|
||||
@@ -41,7 +36,7 @@ export class SingletonCoroutine {
|
||||
// console.log('invoke', arguments)
|
||||
if (this.timeout === null)
|
||||
// noinspection JSCheckFunctionSignatures
|
||||
this.timeout = setTimeout(this.onTimeout, this.delay, this)
|
||||
this.timeout = setTimeout(this.onTimeout, this.delay, this)
|
||||
}
|
||||
|
||||
async onTimeout(self) {
|
||||
@@ -71,7 +66,9 @@ export const uint32max = 4294967295
|
||||
export const uint64max = 18446744073709551615n
|
||||
|
||||
export function tokenNumber(token, balance) {
|
||||
return FixedNumber.fromValue(balance, token.decimals, {decimals: token.decimals, width: 256})
|
||||
const dec = token ? token.decimals : 0
|
||||
console.log('token dec', dec, balance)
|
||||
return FixedNumber.fromValue(balance, dec, {decimals: dec, width: 256})
|
||||
}
|
||||
|
||||
export function tokenFloat(token, balance) {
|
||||
@@ -97,7 +94,7 @@ export function intervalString(seconds) {
|
||||
}
|
||||
|
||||
export function timestampString(seconds) {
|
||||
const date = DateTime.fromSeconds(seconds).setZone(useStore().timeZone)
|
||||
const date = DateTime.fromSeconds(seconds).setZone(usePrefStore().timezone)
|
||||
return dateString(date)
|
||||
}
|
||||
|
||||
@@ -153,10 +150,7 @@ export function inversionPreference(chainId, base, quote) {
|
||||
|
||||
export const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
export function uuid() {
|
||||
// noinspection JSUnresolvedReference
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
export function uuid() {return uuidv4()}
|
||||
|
||||
export function lightenColor(color, lightness = 85, alpha = null) {
|
||||
let c = new Color(color).hsl()
|
||||
@@ -230,6 +224,21 @@ export function intervalToSeconds(interval) {
|
||||
}
|
||||
|
||||
|
||||
export function secondsToInterval(seconds) {
|
||||
const units = [
|
||||
[30 * 24 * 60 * 60, 'M'],
|
||||
[7 * 24 * 60 * 60, 'W'],
|
||||
[24 * 60 * 60, 'D'],
|
||||
[60, ''],
|
||||
[1, 'S'],
|
||||
]
|
||||
for( const [unit, suffix] of units)
|
||||
if (seconds % unit === 0)
|
||||
return `${seconds / unit}${suffix}`
|
||||
throw Error(`invalid secondsToInterval ${seconds}`)
|
||||
}
|
||||
|
||||
|
||||
export function interpolate(a, b, zeroToOne) {
|
||||
const d = (b-a)
|
||||
return a + d * zeroToOne
|
||||
@@ -251,3 +260,39 @@ export function computeInterceptSlope(time0, price0, time1, price1) {
|
||||
export function defined(v) {
|
||||
return v !== undefined && v !== null
|
||||
}
|
||||
|
||||
export function toPrecision(value, significantDigits = 3) {
|
||||
if (!isFinite(value)) return value.toString(); // Handle Infinity and NaN
|
||||
if (value === 0) return "0"; // Special case for 0
|
||||
const magnitude = Math.floor(Math.log10(Math.abs(value)));
|
||||
const decimalsNeeded = Math.max(0, significantDigits - 1 - magnitude);
|
||||
return value.toFixed(decimalsNeeded); // Use toFixed to completely avoid scientific notation
|
||||
}
|
||||
|
||||
export function toPrecisionOrNull(value, significantDigits = 3) {
|
||||
if (value===null) return null
|
||||
if (value===undefined) return undefined
|
||||
return toPrecision(value, significantDigits)
|
||||
}
|
||||
|
||||
export function toHuman(value, significantDigits = 2) {
|
||||
if (!isFinite(value)) return value.toString(); // Handle Infinity and NaN
|
||||
let suffix = ''
|
||||
if (value >= 1_000_000_000) {
|
||||
value /= 1_000_000_000
|
||||
suffix = 'B'
|
||||
}
|
||||
else if (value >= 1_000_000) {
|
||||
value /= 1_000_000
|
||||
suffix = 'M'
|
||||
}
|
||||
else if (value >= 1_000) {
|
||||
value /= 1_000
|
||||
suffix = 'K'
|
||||
}
|
||||
return toPrecision(value, significantDigits) + suffix
|
||||
}
|
||||
|
||||
export function errorSuggestsMissingVault(e) {
|
||||
return e.value === '0x' && e.code === 'BAD_DATA' || e.revert === null && e.code === 'CALL_EXCEPTION';
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import {getToken} from "@/blockchain/token.js";
|
||||
|
||||
let native = false // whether native browser notifications are allowed
|
||||
let notificationsAllowed = false // whether native browser notifications are allowed
|
||||
|
||||
Notification.requestPermission()
|
||||
if ('Notification' in window) {
|
||||
Notification.requestPermission()
|
||||
.then(permission => {
|
||||
console.log(`notification permission: ${permission}`);
|
||||
native = permission === 'granted'
|
||||
notificationsAllowed = permission === 'granted'
|
||||
if (!notificationsAllowed)
|
||||
console.log(`notification permission denied: ${permission}`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`notification permission error: ${error}`);
|
||||
native = false;
|
||||
});
|
||||
.catch(error => {console.error(`notification permission error: ${error}`);});
|
||||
}
|
||||
|
||||
|
||||
export function notify(title, message=null) {
|
||||
if (native) {
|
||||
if (notificationsAllowed) {
|
||||
const options = {
|
||||
renotify: true,
|
||||
tag: title,
|
||||
|
||||
@@ -7,10 +7,6 @@ import {computed, ref} from "vue";
|
||||
import Color from "color";
|
||||
|
||||
|
||||
export const MIN_EXECUTION_TIME = 60 // give at least one full minute for each tranche to trigger
|
||||
export const DEFAULT_SLIPPAGE = 0.0030;
|
||||
|
||||
|
||||
// Builders are data objects which store a configuration state
|
||||
// the component name must match a corresponding Vue component in the BuilderFactory.vue component, which is responsible
|
||||
// for instantiating the UI component for a given builder dictionary, based on its builder.component field.
|
||||
@@ -45,6 +41,7 @@ function newDefaultOrder() {
|
||||
export const useChartOrderStore = defineStore('chart_orders', () => {
|
||||
const chartReady = ref(false)
|
||||
|
||||
const showPoolSelection = ref(false) // if true, the pool information / fee choosing dialog is shown
|
||||
const defaultOrder = newDefaultOrder()
|
||||
const orders = ref([defaultOrder]) // order models in UI format
|
||||
const selectedOrder = ref(null)
|
||||
@@ -56,7 +53,8 @@ export const useChartOrderStore = defineStore('chart_orders', () => {
|
||||
if (!selectedSymbol.value)
|
||||
return null
|
||||
const s = useStore()
|
||||
let result = s.poolPrices[[s.chainId, selectedSymbol.address]]
|
||||
const key = [s.chainId, selectedSymbol.value.address];
|
||||
let result = s.poolPrices[key]
|
||||
if (selectedSymbol.value.inverted)
|
||||
result = 1 / result
|
||||
return result
|
||||
@@ -65,6 +63,7 @@ export const useChartOrderStore = defineStore('chart_orders', () => {
|
||||
const meanRange = ref(1)
|
||||
|
||||
const drawing = ref(false)
|
||||
const drew = ref(true) // true if at least one of the points has been drawn already
|
||||
|
||||
function newOrder() {
|
||||
const order = newDefaultOrder()
|
||||
@@ -94,7 +93,8 @@ export const useChartOrderStore = defineStore('chart_orders', () => {
|
||||
|
||||
return {
|
||||
chartReady, selectedSymbol, intervalSecs, baseToken, quoteToken, price,
|
||||
orders, drawing, newOrder, removeOrder, resetOrders, meanRange,
|
||||
orders, drawing, drew, newOrder, removeOrder, resetOrders, meanRange,
|
||||
showPoolSelection,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -181,6 +181,7 @@ export function timesliceTranches() {
|
||||
const start = Math.floor(i * (duration / Math.max((n - 1), 1)))
|
||||
const end = start + window
|
||||
ts.push(newTranche({
|
||||
marketOrder: true,
|
||||
fraction: amtPerTranche,
|
||||
startTimeIsRelative: true,
|
||||
startTime: start,
|
||||
@@ -193,8 +194,9 @@ export function timesliceTranches() {
|
||||
|
||||
export function builderDefaults(builder, defaults) {
|
||||
for (const k in defaults)
|
||||
if (builder[k] === undefined)
|
||||
if (!Object.prototype.hasOwnProperty.call(builder, k)) {
|
||||
builder[k] = defaults[k] instanceof Function ? defaults[k]() : defaults[k]
|
||||
}
|
||||
}
|
||||
|
||||
export function linearWeights(num, skew) {
|
||||
|
||||
@@ -8,11 +8,15 @@
|
||||
import { loadFonts } from './webfontloader'
|
||||
import vuetify from './vuetify'
|
||||
import {pinia} from '../store/pinia.js'
|
||||
import router from '../router'
|
||||
import {setRouter} from '../router/router.js'
|
||||
import {newRouter} from '../router/newRouter.js'
|
||||
import vsp from "vue-scroll-picker";
|
||||
|
||||
import "vue-scroll-picker/lib/style.css";
|
||||
|
||||
const router = newRouter();
|
||||
setRouter(router)
|
||||
|
||||
export function registerPlugins (app) {
|
||||
loadFonts().catch((e)=>console.error('Could not load fonts!',e))
|
||||
app
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
darken1,
|
||||
darkMiddleShadeIndex,
|
||||
light,
|
||||
lightMiddleShadeIndex, numShades, pageShade,
|
||||
lightMiddleShadeIndex, numShades, surfaceShade,
|
||||
printContrast
|
||||
} from "../../theme.js";
|
||||
|
||||
@@ -28,13 +28,13 @@ function makeColors(isLight) {
|
||||
const ink = k[printContrast] // text color
|
||||
function darken(cols,shades) {return cols[base+(isLight?-shades:shades)]}
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
return {
|
||||
background: k[pageShade],
|
||||
surface: k[pageShade],
|
||||
'surface-bright': k[pageShade],
|
||||
'surface-light': k[pageShade+2],
|
||||
const colors = {
|
||||
background: k[0],
|
||||
surface: k[surfaceShade],
|
||||
'surface-bright': k[surfaceShade],
|
||||
'surface-light': k[surfaceShade+2],
|
||||
'surface-variant': k[14],
|
||||
'on-surface-variant': k[pageShade+2],
|
||||
'on-surface-variant': k[surfaceShade+2],
|
||||
primary: c.greens[base],
|
||||
'primary-darken-1': darken(c.greens, darken1),
|
||||
secondary: c.blues[base],
|
||||
@@ -53,6 +53,8 @@ function makeColors(isLight) {
|
||||
"on-warning": ink,
|
||||
"on-error": ink,
|
||||
}
|
||||
// console.log('colors', isLight?'light':'dark', colors)
|
||||
return colors;
|
||||
}
|
||||
|
||||
const lightColors = makeColors(true)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// Composables
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
component: () => import('@/corp/CorpLayout.vue'),
|
||||
path:'/home',
|
||||
children: [
|
||||
{
|
||||
name: 'Home',
|
||||
path: '/home',
|
||||
component: () => import('@/corp/Home.vue'),
|
||||
},
|
||||
{
|
||||
name: 'HowItWorks',
|
||||
path: '/home/how-it-works',
|
||||
component: () => import('@/corp/HowItWorks.vue'),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/chart/ChartLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'App',
|
||||
path: '/',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('@/components/chart/ChartVault.vue'),
|
||||
},
|
||||
{
|
||||
name: 'Order',
|
||||
path: '/order',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('@/components/chart/ChartPlaceOrder.vue'),
|
||||
},
|
||||
{
|
||||
name: 'Assets',
|
||||
path: '/assets',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('@/components/chart/ChartVault.vue'),
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
path: '/status',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('@/components/chart/ChartStatus.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
10
src/router/newRouter.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Composables
|
||||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
import {routes} from "@/router/routes.js";
|
||||
|
||||
export function newRouter() {
|
||||
return createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes,
|
||||
})
|
||||
}
|
||||
3
src/router/router.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export let router = null
|
||||
|
||||
export function setRouter (r) { router = r }
|
||||
45
src/router/routes.js
Normal file
@@ -0,0 +1,45 @@
|
||||
export const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/chart/ChartLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'App',
|
||||
path: '/',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('@/components/chart/ChartPlaceOrder.vue'),
|
||||
},
|
||||
{
|
||||
name: 'Shared',
|
||||
path: '/shared/:code',
|
||||
component: () => import('@/components/chart/Shared.vue')
|
||||
},
|
||||
{
|
||||
name: 'Order',
|
||||
path: '/order',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('@/components/chart/ChartPlaceOrder.vue'),
|
||||
},
|
||||
{
|
||||
name: 'Assets',
|
||||
path: '/assets',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('@/components/chart/ChartVault.vue'),
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
path: '/status',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('@/components/chart/ChartStatus.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
88
src/share.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import {changeIntervalSecs, onChartReady, setSymbol, widget} from "@/charts/chart.js";
|
||||
import {usePrefStore, useStore} from "@/store/store.js";
|
||||
import {lookupSymbol} from "@/charts/datafeed.js";
|
||||
import {track} from "@/track.js";
|
||||
|
||||
import {socket} from "@/socket.js";
|
||||
|
||||
export async function getShareUrl() {
|
||||
const co = useChartOrderStore();
|
||||
const s = useStore()
|
||||
const sym = co.selectedSymbol
|
||||
console.log('symbol', sym)
|
||||
const data = {
|
||||
version: 1,
|
||||
chainId: s.chainId,
|
||||
orders: co.orders,
|
||||
symbol: {
|
||||
base: {a: sym.base.a, s: sym.base.s},
|
||||
quote: {a: sym.quote.a, s: sym.quote.s},
|
||||
route: {
|
||||
fee: sym.fee,
|
||||
exchange: sym.exchangeId,
|
||||
}
|
||||
},
|
||||
period: co.intervalSecs,
|
||||
}
|
||||
const json = JSON.stringify(data)
|
||||
console.log('sharing data', json, data)
|
||||
const snapshot = await takeSnapshot()
|
||||
|
||||
const code = await new Promise((resolve)=>socket.emit('share', data, snapshot, resolve))
|
||||
if (code===null) return null
|
||||
|
||||
return import.meta.env.VITE_SHARE_URL+ '/share/'+code;
|
||||
}
|
||||
|
||||
export async function loadShareUrl(code) {
|
||||
// console.log('loading share url', code)
|
||||
const data = await new Promise((resolve, reject) => {
|
||||
// Set a timeout (e.g., 8 seconds)
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Timed out waiting for response from server'));
|
||||
}, 8000);
|
||||
|
||||
socket.emit('shared', code, (response) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(response);
|
||||
});
|
||||
}).catch(err => {
|
||||
// Optional: show error to user or log it
|
||||
console.error('Failed to load shared URL:', err.message);
|
||||
return null;
|
||||
});
|
||||
if (data===null) return false
|
||||
console.log('loaded shared orders data', data)
|
||||
const co = useChartOrderStore();
|
||||
const s = useStore()
|
||||
const ticker = `${data.chainId}|${data.symbol.route.exchange}|${data.symbol.base.a}|${data.symbol.quote.a}|${data.symbol.route.fee}`;
|
||||
const symbol = lookupSymbol(ticker)
|
||||
if (symbol===null) {
|
||||
console.error('could not find symbol for ticker', ticker)
|
||||
return false
|
||||
}
|
||||
s.chainId = data.chainId
|
||||
const prefs = usePrefStore()
|
||||
prefs.selectedSymbol = ticker
|
||||
for (const order of data.orders) {
|
||||
// force amount to be zero so that the user MUST enter a size before confirming the placement
|
||||
order.amount = 0
|
||||
order.amountIsTokenA = true
|
||||
order.valid = false
|
||||
}
|
||||
co.orders = data.orders
|
||||
changeIntervalSecs(data.period)
|
||||
onChartReady(()=>{
|
||||
setSymbol(symbol)
|
||||
.catch((e)=>console.error('could not set symbol', e))
|
||||
})
|
||||
track('shared')
|
||||
console.log('loaded orders', s.chainId, co.orders)
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function takeSnapshot() {
|
||||
const screenshotCanvas = await widget.takeClientScreenshot();
|
||||
return await new Promise((resolve) => screenshotCanvas.toBlob(resolve));
|
||||
}
|
||||
147
src/socket.js
@@ -1,145 +1,8 @@
|
||||
import {io} from "socket.io-client";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {flushOrders} from "@/blockchain/wallet.js";
|
||||
import {parseElaboratedOrderStatus} from "@/blockchain/orderlib.js";
|
||||
import { DataFeed } from "./charts/datafeed";
|
||||
import {notifyFillEvent} from "@/notify.js";
|
||||
|
||||
export const socket = io(import.meta.env.VITE_WS_URL || undefined, {transports: ["websocket"]})
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log(new Date(), 'ws connected')
|
||||
useStore().connected = true
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log(new Date(), 'ws disconnected')
|
||||
useStore().connected = false
|
||||
})
|
||||
|
||||
socket.on('approvedRegion', (approved) => {
|
||||
console.log('approved region', approved)
|
||||
useStore().regionApproved = approved
|
||||
})
|
||||
|
||||
socket.on('approvedWallet', (approved) => {
|
||||
console.log('approved wallet', approved)
|
||||
useStore().walletApproved = approved
|
||||
})
|
||||
|
||||
socket.on('p', async (chainId, pool, price) => {
|
||||
console.log('pool price from message', chainId, pool, price)
|
||||
const s = useStore()
|
||||
if( s.chainId !== chainId )
|
||||
return
|
||||
s.poolPrices[[chainId,pool]] = price
|
||||
})
|
||||
|
||||
socket.on('ohlc', async (chainId, poolPeriod, ohlcs) => {
|
||||
// console.log('pool bars', poolPeriod, ohlcs)
|
||||
if (ohlcs && ohlcs.length) {
|
||||
const split = poolPeriod.indexOf('|')
|
||||
const pool = poolPeriod.slice(0,split)
|
||||
useStore().poolPrices[[chainId, pool]] = parseFloat(ohlcs[ohlcs.length - 1][4]) // closing price
|
||||
}
|
||||
DataFeed.poolCallback(chainId, poolPeriod, ohlcs)
|
||||
})
|
||||
|
||||
socket.on('vb', async (chainId, vault, balances) => {
|
||||
const s = useStore()
|
||||
if( s.chainId !== chainId )
|
||||
return
|
||||
console.log('vb', vault, balances)
|
||||
s.vaultBalances[vault] = JSON.parse(balances)
|
||||
console.log('vault balances', vault, s.vaultBalances[vault])
|
||||
})
|
||||
|
||||
socket.on('vaults', (chainId, owner, vaults)=>{
|
||||
const s = useStore()
|
||||
console.log('vaults', chainId, owner, vaults)
|
||||
if( s.chainId !== chainId || s.account !== owner )
|
||||
return
|
||||
if( vaults.length > s.vaults.length ) {
|
||||
s.vaults = vaults
|
||||
if( vaults.length ) {
|
||||
const vault = vaults[0]
|
||||
flushOrders(chainId, owner, 0, vault)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function handleOrderStatus(chainId, vault, orderIndex, status) {
|
||||
const s = useStore()
|
||||
if( s.chainId !== chainId )
|
||||
return
|
||||
// message 'o' is a single order status
|
||||
const parsed = parseElaboratedOrderStatus(chainId, status);
|
||||
console.log('o', chainId, vault, orderIndex, status, parsed)
|
||||
if( !(vault in s.orders) )
|
||||
s.orders[vault] = {}
|
||||
s.orders[vault][orderIndex] = parsed
|
||||
const socketOptions = {
|
||||
transport: ['polling', 'websocket'],
|
||||
pingInterval: 25000, // PING every 25 seconds
|
||||
pingTimeout: 60000 // Timeout if no PONG in 60 seconds
|
||||
}
|
||||
|
||||
socket.on('os', (chainId, vault, orders) => {
|
||||
// message 'os' has multiple order statuses
|
||||
console.log('os', orders)
|
||||
for( const [orderIndex, status] of orders )
|
||||
handleOrderStatus(chainId, vault, orderIndex, status)
|
||||
})
|
||||
|
||||
socket.on( 'o', handleOrderStatus)
|
||||
|
||||
socket.on( 'of', (chainId, vault, orderIndex, filled)=>{
|
||||
const s = useStore()
|
||||
if( s.chainId !== chainId )
|
||||
return
|
||||
console.log('of', chainId, vault, orderIndex, filled)
|
||||
if( !(vault in s.orders) ) {
|
||||
console.log('warning: got fill on an order in an unknown vault')
|
||||
return
|
||||
}
|
||||
if( !(orderIndex in s.orders[vault]) ) {
|
||||
console.log(`warning: orderIndex ${orderIndex} missing from vault ${vault}`)
|
||||
return
|
||||
}
|
||||
|
||||
const status = s.orders[vault][orderIndex]
|
||||
console.log('apply fills', status, filled)
|
||||
|
||||
let orderIn = 0n
|
||||
let orderOut = 0n
|
||||
for (const i in filled) {
|
||||
const ts = status.trancheStatus[i]
|
||||
let filledIn = 0n
|
||||
let filledOut = 0n
|
||||
const [activationTime, fills] = filled[i];
|
||||
const numOld = ts.fills.length;
|
||||
for (let i=0; i<fills.length; i++) {
|
||||
const fill = fills[i]
|
||||
let [tx, time, fi, fo, fee] = fill
|
||||
fi = BigInt(fi)
|
||||
fo = BigInt(fo)
|
||||
fee = BigInt(fee)
|
||||
filledIn += fi
|
||||
filledOut += fo
|
||||
if (i>=numOld) {
|
||||
// new fill detected
|
||||
const f = {tx, time, filledIn: fi, filledOut: fo, fee, filled: status.order.amountIsInput ? fi : fo};
|
||||
console.log('new fill', f)
|
||||
notifyFillEvent(chainId, status, i, f).catch((e)=>console.log('fill notification error', e))
|
||||
ts.fills.push(f)
|
||||
}
|
||||
}
|
||||
ts.filledIn = filledIn
|
||||
ts.filledOut = filledOut
|
||||
ts.activationTime = activationTime
|
||||
orderIn += filledIn
|
||||
orderOut += filledOut
|
||||
}
|
||||
status.filledIn = orderIn
|
||||
status.filledOut = orderOut
|
||||
status.filled = status.order.amountIsInput ? orderIn : orderOut
|
||||
|
||||
console.log('apply fills completed', status)
|
||||
})
|
||||
export const socket = io(import.meta.env.VITE_WS_URL || undefined, socketOptions)
|
||||
|
||||
165
src/socketInit.js
Normal file
@@ -0,0 +1,165 @@
|
||||
import {socket} from "@/socket.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {flushWalletTransactions} from "@/blockchain/wallet.js";
|
||||
import {parseElaboratedOrderStatus} from "@/blockchain/orderlib.js";
|
||||
import {DataFeed} from "./charts/datafeed";
|
||||
import {notifyFillEvent} from "@/notify.js";
|
||||
import {refreshOHLCSubs} from "@/blockchain/ohlcs.js";
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('ws connected')
|
||||
const s = useStore();
|
||||
s.connected = true
|
||||
if (s.chainId && s.account)
|
||||
socket.emit('address', s.chainId, s.account)
|
||||
refreshOHLCSubs()
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('ws disconnected')
|
||||
useStore().connected = false
|
||||
})
|
||||
|
||||
socket.on('connect_error', (err) => {
|
||||
console.log('ws connect error', err)
|
||||
useStore().connected = false
|
||||
})
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.log('ws error', err)
|
||||
useStore().connected = false
|
||||
})
|
||||
|
||||
socket.on('approvedRegion', (approved) => {
|
||||
console.log('approved region', approved)
|
||||
useStore().regionApproved = approved
|
||||
})
|
||||
|
||||
socket.on('approvedWallet', (approved) => {
|
||||
console.log('approved wallet', approved)
|
||||
useStore().walletApproved = approved
|
||||
})
|
||||
|
||||
socket.on('p', async (chainId, pool, price) => {
|
||||
console.log('pool price from message', chainId, pool, price)
|
||||
const s = useStore()
|
||||
if( s.chainId !== chainId )
|
||||
return
|
||||
s.poolPrices[[chainId,pool]] = price
|
||||
})
|
||||
|
||||
socket.on('ohlc', async (chainId, poolPeriod, ohlcs) => {
|
||||
// console.log('pool bars', poolPeriod, ohlcs)
|
||||
if (ohlcs && ohlcs.length) {
|
||||
const split = poolPeriod.indexOf('|')
|
||||
const pool = poolPeriod.slice(0,split)
|
||||
useStore().poolPrices[[chainId, pool]] = parseFloat(ohlcs[ohlcs.length - 1][4]) // closing price
|
||||
}
|
||||
DataFeed.poolCallback(chainId, poolPeriod, ohlcs)
|
||||
})
|
||||
|
||||
socket.on('vb', async (chainId, vault, balances) => {
|
||||
const s = useStore()
|
||||
if( s.chainId !== chainId )
|
||||
return
|
||||
console.log('vb', vault, balances)
|
||||
s.vaultBalances[vault] = JSON.parse(balances)
|
||||
console.log('vault balances', vault, s.vaultBalances[vault])
|
||||
})
|
||||
|
||||
socket.on('vaults', (chainId, owner, vaults)=>{
|
||||
const s = useStore()
|
||||
console.log('vaults', chainId, owner, vaults)
|
||||
if( s.chainId !== chainId || s.account !== owner )
|
||||
return
|
||||
if( vaults.length > s.vaults.length ) {
|
||||
s.vaults = vaults
|
||||
if( vaults.length ) {
|
||||
const vault = vaults[0]
|
||||
flushWalletTransactions(chainId, owner, 0, vault)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('mark.usd', (chainId, token, value)=>{
|
||||
const s = useStore()
|
||||
s.markPrices[`${chainId}|${token}`] = Number(value)
|
||||
// console.log('mark.usd', token, value)
|
||||
})
|
||||
|
||||
function handleOrderStatus(chainId, vault, orderIndex, status) {
|
||||
const s = useStore()
|
||||
if( s.chainId !== chainId )
|
||||
return
|
||||
// message 'o' is a single order status
|
||||
const parsed = parseElaboratedOrderStatus(chainId, status);
|
||||
// console.log('o', chainId, vault, orderIndex, status, parsed)
|
||||
if( !(vault in s.orders) )
|
||||
s.orders[vault] = {}
|
||||
s.orders[vault][orderIndex] = parsed
|
||||
}
|
||||
|
||||
socket.on('os', (chainId, vault, orders) => {
|
||||
// message 'os' has multiple order statuses
|
||||
console.log('os', orders)
|
||||
for( const [orderIndex, status] of orders )
|
||||
handleOrderStatus(chainId, vault, orderIndex, status)
|
||||
})
|
||||
|
||||
socket.on( 'o', handleOrderStatus)
|
||||
|
||||
socket.on( 'of', (chainId, vault, orderIndex, filled)=>{
|
||||
const s = useStore()
|
||||
if( s.chainId !== chainId )
|
||||
return
|
||||
console.log('of', chainId, vault, orderIndex, filled)
|
||||
if( !(vault in s.orders) ) {
|
||||
console.log('warning: got fill on an order in an unknown vault')
|
||||
return
|
||||
}
|
||||
if( !(orderIndex in s.orders[vault]) ) {
|
||||
console.log(`warning: orderIndex ${orderIndex} missing from vault ${vault}`)
|
||||
return
|
||||
}
|
||||
|
||||
const status = s.orders[vault][orderIndex]
|
||||
console.log('apply fills', status, filled)
|
||||
|
||||
let orderIn = 0n
|
||||
let orderOut = 0n
|
||||
for (const i in filled) {
|
||||
const ts = status.trancheStatus[i]
|
||||
let filledIn = 0n
|
||||
let filledOut = 0n
|
||||
const [activationTime, fills] = filled[i];
|
||||
const numOld = ts.fills.length;
|
||||
for (let i=0; i<fills.length; i++) {
|
||||
const fill = fills[i]
|
||||
let [tx, time, fi, fo, fee] = fill
|
||||
fi = BigInt(fi)
|
||||
fo = BigInt(fo)
|
||||
fee = BigInt(fee)
|
||||
filledIn += fi
|
||||
filledOut += fo
|
||||
if (i>=numOld) {
|
||||
// new fill detected
|
||||
const f = {tx, time, filledIn: fi, filledOut: fo, fee, filled: status.order.amountIsInput ? fi : fo};
|
||||
console.log('new fill', f)
|
||||
notifyFillEvent(chainId, status, i, f).catch((e)=>console.log('fill notification error', e))
|
||||
ts.fills.push(f)
|
||||
}
|
||||
}
|
||||
ts.filledIn = filledIn
|
||||
ts.filledOut = filledOut
|
||||
ts.activationTime = activationTime
|
||||
orderIn += filledIn
|
||||
orderOut += filledOut
|
||||
}
|
||||
status.filledIn = orderIn
|
||||
status.filledOut = orderOut
|
||||
status.filled = status.order.amountIsInput ? orderIn : orderOut
|
||||
|
||||
console.log('apply fills completed', status)
|
||||
})
|
||||
|
||||
console.log('initialized socketio')
|
||||
@@ -38,7 +38,6 @@ const REQUIRE_APPROVAL = import.meta.env.VITE_REQUIRE_APPROVAL !== 'NO';
|
||||
export const useStore = defineStore('app', ()=> {
|
||||
const clock = ref(timestamp()) // the clock ticks infrequently enough to be mostly stable for user display
|
||||
setInterval(()=>clock.value=timestamp(), 10*1000) // 10 secs
|
||||
const timeZone = ref('Etc/UTC')
|
||||
|
||||
const nav = ref(false) // controls opening navigation drawer
|
||||
const theme = ref('dark')
|
||||
@@ -102,11 +101,14 @@ export const useStore = defineStore('app', ()=> {
|
||||
const orders = ref({}) // indexed by vault value is another dictionary with orderIndex as key and order status values
|
||||
|
||||
const vault = computed(() => vaults.value.length === 0 ? null : vaults.value[0] )
|
||||
const creatingVault = ref(false) // used when a vault is first created but not yet responsive via metamask. If vault is not null, but creatingVault is true, then the vault is not yet ready for interaction.
|
||||
const upgrade = ref(null)
|
||||
const version = computed( () => vaultVersions.value.length === 0 ? 0 : vaultVersions.value[0] )
|
||||
const balances = computed( () => vault.value === null ? {} : vaultBalances.value[vault.value] || {} )
|
||||
const vaultOrders = computed(()=> vault.value === null || (!vault.value in orders.value) ? {} : orders.value[vault.value] ? orders.value[vault.value] : [] )
|
||||
const tokens = computed(getTokens)
|
||||
const markPrices = ref({}) // key: `${chainId}|${tokenAddr}` value: USD value
|
||||
function markPrice(token) { return markPrices.value[`${chainId.value}|${token}`] }
|
||||
const factory = computed(() => !chain.value ? null : chain.value.factory)
|
||||
const helper = computed(() => {console.log('chain helper', chain.value); return !chain.value ? null : chain.value.helper})
|
||||
const mockenv = computed(() => !chain.value ? null : chain.value.mockenv)
|
||||
@@ -133,14 +135,21 @@ export const useStore = defineStore('app', ()=> {
|
||||
this.extraTokens = extras
|
||||
}
|
||||
|
||||
function getBalance(tokenAddr) {
|
||||
const found = this.balances[tokenAddr]
|
||||
return found === undefined ? 0 : found
|
||||
}
|
||||
|
||||
return {
|
||||
connected,
|
||||
nav, chainId, chainInfo, chain, provider, providerRef, vaultInitCodeHash, account, vaults, vaultVersions,
|
||||
transactionSenders, errors, extraTokens, poolPrices, vaultBalances, orders, vault, version, upgrade, vaultOrders,
|
||||
tokens, factory, helper, theme,
|
||||
mockenv, mockCoins,
|
||||
removeTransactionSender, error, closeError, addToken, clock, timeZone, balances,
|
||||
removeTransactionSender, error, closeError, addToken, clock, balances,
|
||||
approved, regionApproved, walletApproved,
|
||||
getBalance, creatingVault,
|
||||
markPrices, markPrice,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -206,8 +215,12 @@ export const usePrefStore = defineStore({
|
||||
state: ()=> {
|
||||
// user preferences
|
||||
const inverted = ref({})
|
||||
const hints = ref({})
|
||||
const newbie = ref(true)
|
||||
const acceptedTos = ref('NO TOS ACCEPTED')
|
||||
return {inverted, acceptedTos,}
|
||||
const selectedTicker = ref(null)
|
||||
const selectedTimeframe = ref(null)
|
||||
const timezone = ref('Etc/UTC')
|
||||
return {inverted, acceptedTos, selectedTicker, selectedTimeframe, timezone, newbie, hints, }
|
||||
},
|
||||
})
|
||||
|
||||
|
||||