Compare commits

54 Commits

Author SHA1 Message Date
tim
ed03740852 fixed weight slider bounds 2025-05-19 16:02:01 -04:00
tim
a3c1dfad2d put app back on app.dexorder.com and corp site on dexorder.com with www redirecting to apex 2025-05-19 15:19:20 -04:00
tim
9b410ace09 mobile screen alert 2025-05-08 17:34:05 -04:00
tim
72b061749d withdraw fix; wrap still broken 2025-05-07 21:02:05 -04:00
tim
a42f228984 support chat 2025-05-06 22:27:30 -04:00
tim
47b33a152d crisp for the dotcom 2025-05-06 18:47:34 -04:00
tim
ecffd976ac dotcom 2025-05-06 15:28:19 -04:00
tim
f0f431b34e dotcom 2025-05-06 13:56:05 -04:00
tim
b2457f0617 tab switch bugfix; remove floating div 2025-05-01 11:16:45 -04:00
tim
148b37dca3 removed vite url debug 2025-04-28 16:24:49 -04:00
tim
1b83dad6b3 spam log removal; mobile share fixes 2025-04-26 17:52:04 -04:00
tim
a1bbb17b5d spam log removal; mobile share fixes 2025-04-26 17:51:20 -04:00
tim
916c23e092 mobile fixes 2025-04-26 15:00:12 -04:00
tim
2c5116c5de mobile fixes 2025-04-26 14:56:55 -04:00
tim
441a514acc short share urls 2025-04-23 12:55:49 -04:00
tim
6f7388bb10 share.dexorder.trade instead of ws 2025-04-22 17:43:16 -04:00
tim
fd7b9713ea share landing page bugfixes 2025-04-22 17:21:12 -04:00
tim
7b5421e6e7 share tracking 2025-04-22 16:53:26 -04:00
tim
21f324aa12 better unlisted token handling 2025-04-22 16:45:44 -04:00
tim
eeee9d9853 order sharing 2025-04-22 16:18:32 -04:00
tim
38fb66c694 order sharing 2025-04-22 16:15:14 -04:00
tim
14b8b50812 more tracking; hide arb from welcome splash 2025-04-15 19:21:39 -04:00
tim
f35b30e337 removed debug span 2025-04-14 14:40:53 -04:00
tim
22f2e648a2 one-time hints; builder touchups 2025-04-11 21:31:04 -04:00
tim
7973a1e8b7 order selection touchup 2025-04-10 17:55:51 -04:00
tim
c50824adb6 fill shape color touchup 2025-04-10 17:51:40 -04:00
tim
257c476cc1 order status click row to select 2025-04-10 14:37:36 -04:00
tim
815109dec2 order status click row to select 2025-04-10 14:31:49 -04:00
tim
0673b01ac8 "breakdown" language for breakout sells 2025-04-10 13:34:48 -04:00
tim
556554fbf3 welcome dialog; order UI facelift 2025-04-09 20:57:29 -04:00
tim
94c7b6ddb4 removed beta tag 2025-04-02 16:36:46 -04:00
tim
715a43c097 bugfix for limit/diagonal tranche building 2025-04-01 13:48:59 -04:00
tim
0442b08623 new pool selection dialog and liquidity 2025-03-30 21:50:24 -04:00
tim
0392e70b78 USD marks 2025-03-29 15:27:13 -04:00
tim
a6bce1613b transaction progressor 2025-03-28 20:05:31 -04:00
tim
7626504480 MarketBuilder slippage fix 2025-03-28 19:59:22 -04:00
tim
e86fbfa8e9 DCABuilder slippage parameter fix 2025-03-26 23:17:28 -04:00
tim
7d04d23a89 drag zoom bugfix 2025-03-26 17:10:18 -04:00
tim
dabf6dd60f land on Order page; ui fixes 2025-03-26 16:59:22 -04:00
tim
b1a864ce31 DCABuilder UI tweak 2025-03-20 17:50:34 -04:00
tim
75a197947f new DCABuilder bugfix 2025-03-20 17:02:47 -04:00
tim
d446d5ab11 fees.js; DCABuilder gas warning 2025-03-20 14:10:09 -04:00
tim
5a4a67e726 rate limit DCA 2025-03-19 21:04:24 -04:00
tim
8750951de5 rate limit DCA 2025-03-19 20:27:10 -04:00
tim
cd84e7c3c9 reverted dragging to old method 2025-03-17 14:53:31 -04:00
tim
5876efe29f order sanity checks 2025-03-16 21:15:00 -04:00
tim
b9975cda10 DCA "breakout" fix 2025-03-13 11:39:38 -04:00
tim
826177c445 save selected symbol & timeframe 2025-03-11 15:21:00 -04:00
tim
3baa74174d minAmount 0.1% 2025-03-11 10:51:05 -04:00
tim
b2ed48492b allocationText fix 2025-03-10 21:12:52 -04:00
tim
ebf70dd10c intro vid on homepage 2025-03-05 18:12:34 -04:00
tim
488e9f45f1 underfunded 2025-03-03 22:06:55 -04:00
tim
f5f53c6af4 re-establish subscriptions after ws disconnect 2025-02-26 17:28:23 -04:00
tim
2e49346533 allocationText fix 2025-02-23 10:25:14 -04:00
102 changed files with 3531 additions and 1000 deletions

395
.dependency-cruiser.cjs Normal file
View 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

View File

@@ -1 +1,2 @@
VITE_WS_URL=wss://ws.dexorder.trade
VITE_WS_URL=wss://ws.dexorder.com
VITE_SHARE_URL=https://app.dexorder.com

View File

@@ -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
View File

@@ -0,0 +1,2 @@
#!/bin/bash
npx depcruise src

View File

@@ -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>

View File

@@ -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",

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

11
public/uniswap-logo.svg Normal file
View 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

View File

@@ -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>

View File

@@ -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/')

View File

@@ -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])
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -0,0 +1,3 @@
export let provider = null
export function setProvider(p) {provider = p}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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
}
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))
try {
const tx = toRaw(await this.createTx(contract))
this.signed(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)
}
}

View 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,
}

View File

@@ -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 = []
discoverVaults(addr)
flushTransactions()
// one of these two methods will call flushTransactions()
if (useWalletStore().transaction!==null)
ensureVault()
else
discoverVaults(addr)
socket.emit('address', chainId, addr)
}
}
@@ -99,17 +121,17 @@ export function detectChain() {
try {
window.ethereum.on('chainChanged', onChainChanged);
window.ethereum.on('accountsChanged', onAccountsChanged);
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)
return
}
new ethers.BrowserProvider(window.ethereum).getNetwork().then((network)=>{
const chainId = network.chainId
onChainChanged(chainId)
})
}
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)
}
}

View File

@@ -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}))
}

View File

@@ -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
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()
})
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
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)

View File

@@ -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

View File

@@ -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'}`)

View File

@@ -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 = allocationText(buy, 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)

View File

@@ -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,31 +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(buy, 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)
if (buy===undefined)
console.error('allocation text buy', buy)
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={}) {
@@ -110,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
@@ -132,6 +137,8 @@ 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))
@@ -164,9 +171,9 @@ export class Shape {
newProps.linecolor = color
// text label
let text = allocationText(this.model.buy, 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
@@ -324,14 +331,16 @@ export class Shape {
}
// diagonals need to override this to adjust their price as well.
pointsToTvOhlcStart(points, periodSeconds=null) {
return points === null ? null : points.map((p) => {
return {time: nearestOhlcStart(p.time, periodSeconds), price: p.price}
})
}
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)
@@ -381,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
@@ -476,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())
}
}
}
@@ -653,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}])
}
}

View File

@@ -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)
),
]);
}

View File

@@ -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 },

View File

@@ -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>

View File

@@ -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()

View File

@@ -1,5 +1,7 @@
<template>
<!--
<v-chip text="BETA" size='x-small' color="red" class="align-self-start" variant="text"/>
-->
</template>
<script>
export default {

View File

@@ -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)

View 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>

View File

@@ -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;

View 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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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

View 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>

View File

@@ -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()

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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 Dexorders 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>

View File

@@ -1,8 +1,8 @@
<template>
<v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1" max="255"
v-model="os.tranches" :rules="[validateRequired,validateTranches]" v-auto-select>
<template v-slot:append-inner>tranches</template>
</v-text-field>
<v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1" max="255"
v-model="os.tranches" :rules="[validateRequired,validateTranches]" v-auto-select>
<template v-slot:append-inner>tranches</template>
</v-text-field>
<v-text-field type="number" variant="outlined" :min="1" v-model="os.interval" class="interval"
:label="os.intervalIsTotal ? 'Completion time' : 'Time between tranches'" v-auto-select>
<template v-slot:prepend-inner>

View File

@@ -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
}

View 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>

View File

@@ -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() {

View File

@@ -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

View File

@@ -1,29 +1,32 @@
<template>
<row-bar :color="builder.color">
<color-band :color="builder.color"/>
<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>
<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">&nbsp;</div>
<slot/>
<div class="align-self-center ml-auto mr-3 trashcan">
<v-btn icon="mdi-delete" @click="showDeleteDialog=true"/>
</div>
</div>
</row-bar>
<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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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,25 +153,92 @@ 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>
<style lang="scss"> // NOT scoped

View File

@@ -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()

View File

@@ -1,245 +1,301 @@
<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,
buy: true,
color: color.value,
valid: true,
})
const times = ref([])
const weights = ref([])
function adjustShapes() {}
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, amountSymbol.value)))
function buildTranches() {
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.`)
}
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 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 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)
}
})
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}`
}))
const sched = ref(null)
const schedFetcher = new SingletonCoroutine(async (vault)=>sched.value = vault === null ? null : await getFeeSchedule(vault))
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 partsGasHint = computed(()=>{
if (sched.value === null) {
schedFetcher.invoke(s.vault)
return null
}
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])
const absTimeA = computed({
get() { return _timeEndpoints.value[0] },
function toggleTimeUnits() {
timeUnitIndex.value += 1
}
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) {
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)
emitUpdate({endTime: v, interval: Math.abs(v - startTime.value)})
}
})
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)
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 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])
}
}
}
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 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 updateA(a) {
update(a, _timeEndpoints.value[1], true)
function deleteShapes() {
deleteShapeId(shapeId)
}
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 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)
}
return tranches
}
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">
@@ -247,5 +303,11 @@ td.weight {
padding-left: 1em;
padding-right: 1em;
}
.parts {
width: 10em;
}
.interval {
width: 22em;
}
</style>

View 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>

View File

@@ -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>&nbsp;</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,7 +129,7 @@ builderDefaults(props.builder, {
extendLeft: false,
extendRight: true,
rungs: 1,
skew: 0,
balance: 0,
breakout: false,
color: defaultColor,
buy: true,
@@ -132,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()
@@ -149,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)
@@ -157,7 +169,7 @@ function buildTranches() {
}
// if( flipped.value )
// tranches.reverse()
return tranches
return {tranches, warnings}
}
@@ -198,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(
@@ -220,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(
@@ -242,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(
@@ -264,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(
@@ -277,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
}
}
@@ -349,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(props.order.buy, 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])
@@ -399,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>

View File

@@ -0,0 +1,11 @@
<template>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>

View File

@@ -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,7 +76,7 @@ builderDefaults(props.builder, {
priceA: null,
priceB: null,
rungs: 1,
skew: 0,
balance: 0,
breakout: false,
color: defaultColor,
buy: true,
@@ -81,6 +86,7 @@ function buildTranches() {
const order = props.order
const builder = props.builder
const tranches = []
const warnings = []
console.log('buildTranches', builder, order, tranches)
const ps = prices.value
@@ -89,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}
}
@@ -134,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(()=>{
@@ -147,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
@@ -168,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
@@ -196,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(props.order.buy, 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) {
@@ -228,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>

View File

@@ -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)

View 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>

View File

@@ -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,16 +53,31 @@
<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"
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">
<template v-slot:prepend>
<v-btn icon="mdi-scale-balance" variant="plain" @click="builder.skew=0" :color="color"/>
</template>
</v-text-field>
<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/>
</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.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,6 +382,7 @@ 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,
@@ -414,6 +467,8 @@ function deleteShapes() {
if (!endpoints.value[0])
shapeA.createOrDraw(); // initiate drawing mode
else
adjustShapes()
</script>
@@ -427,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;
}

View 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>

View File

@@ -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'])

View File

@@ -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>

View File

@@ -4,7 +4,7 @@
<script setup>
function openApp() {
window.open('https://app.dexorder.trade/', 'dexorderapp')
window.open('https://app.dexorder.com/', 'dexorderapp')
}
</script>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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
View 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
View 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()

View File

@@ -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()

View File

@@ -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>

View File

@@ -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() }

View File

@@ -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';
}

View File

@@ -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()
.then(permission => {
console.log(`notification permission: ${permission}`);
native = permission === 'granted'
})
.catch(error => {
console.error(`notification permission error: ${error}`);
native = false;
});
if ('Notification' in window) {
Notification.requestPermission()
.then(permission => {
notificationsAllowed = permission === 'granted'
if (!notificationsAllowed)
console.log(`notification permission denied: ${permission}`);
})
.catch(error => {console.error(`notification permission error: ${error}`);});
}
export function notify(title, message=null) {
if (native) {
if (notificationsAllowed) {
const options = {
renotify: true,
tag: title,

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)

View File

@@ -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
View 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
View File

@@ -0,0 +1,3 @@
export let router = null
export function setRouter (r) { router = r }

45
src/router/routes.js Normal file
View 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
View 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));
}

View File

@@ -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
View 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')

View File

@@ -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, }
},
})

View File

@@ -1,4 +1,5 @@
// these must also be set in vuetify.js for the "theme"
// see src/plugins/vuetify.js esp. makeColors()
@use 'sass:color';
// OFFICIAL DEXORDER PALETTE

17
src/track.js Normal file
View File

@@ -0,0 +1,17 @@
export let tracking_enabled = window.gtag !== undefined
if(tracking_enabled) {
// console.log('gtag', tracking_enabled)
}
else {
console.log('tracking disabled')
}
export function track(...args) {
if (tracking_enabled) {
try {
window.gtag('event', ...args)
} catch (e) {
}
}
}

View File

@@ -16,8 +16,14 @@ const versionPromise = fetch('/contract/version.json').then(_json('version.json'
const metadataPromise = fetch('/metadata.json').then(_json('metadata.json'))
export const version = await versionPromise
console.log('version', version)
// console.log('version', version)
export const metadata = await metadataPromise
console.log('metadata', metadata)
// console.log('metadata', metadata)
export function dexorderAddress(chainId) { return version['chainInfo'][chainId]['dexorder'] }
export function factoryAddress(chainId) { return version['chainInfo'][chainId]['factory'] }
export function helperAddress(chainId) { return version['chainInfo'][chainId]['helper'] }
export function vaultInitCodeHash(chainId) { return version['chainInfo'][chainId]['vaultInitCodeHash'] }
// maps [chainId][addr] to pool or token metadata
export const metadataMap = buildMetadataMap(metadata)

View File

@@ -24,7 +24,7 @@ export const darkMiddleShadeIndex = 9
export const numShades = 20 // if you change this, see vuetify.js colors that hardcode indexes
// these parameters are expressed in terms of numShades:
export const pageShade = 2
export const surfaceShade = 3
export const printContrast = 15;
// vuetify darken. values are added/substracted from the middleShadeIndex. use positive numbers here.
export const colorContrast = 4;

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