Compare commits

77 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
tim
0fdc45a031 moved media to corp 2025-02-23 10:13:18 -04:00
tim
956d79c3dc updated allocationText() 2025-02-23 10:12:59 -04:00
tim
2397ebfe45 bugfixes 2025-02-21 23:37:29 -04:00
tim
366531c185 font comments 2025-02-17 11:54:52 -04:00
tim
fdea1402c6 Terms and Fees 2025-02-16 15:30:23 -04:00
tim
a38092b1a9 USDC.e quote symbol 2025-02-11 15:28:27 -04:00
tim
f67685afc3 fixed corp scrollbar 2025-02-06 13:26:49 -04:00
tim
d9ba46fb5d logo fix 2025-02-06 12:42:16 -04:00
tim
e706d8adbb failed approval requests delay longer 2025-02-06 09:15:00 -04:00
tim
5d8f0235d2 app.dexorder.trade 2025-02-06 08:42:54 -04:00
tim
43c5f3bd52 db records TOS acceptance 2025-01-30 12:25:15 -04:00
tim
273b877079 package upgrade 2025-01-28 01:10:01 -04:00
tim
0545cd6e97 darkmode 2025-01-16 20:17:03 -04:00
tim
19a8ffbbd4 MaxMind IP database & region approvals 2024-12-19 20:18:56 -04:00
tim
a689766f37 ToS; persistent pref store; package upgrade 2024-11-19 21:23:00 -04:00
tim
bb4f7d4607 OHLC load fixes; package updates 2024-11-16 19:30:02 -04:00
tim
a9bf23ddbb transaction placement dialog 2024-11-11 20:55:28 -04:00
tim
43891434c5 language touchup 2024-11-06 16:50:39 -04:00
tim
99a6cc1742 chart config tweak 2024-11-05 16:58:57 -04:00
tim
28dd64b1cf post-order line draw improvements 2024-11-04 18:39:09 -04:00
tim
cfcba95445 buy/sell color change fix 2024-11-04 14:39:43 -04:00
tim
61101fcf0a updated Home features 2024-10-31 20:12:13 -04:00
tim
a7a1628f3c updated Home features 2024-10-31 20:10:08 -04:00
130 changed files with 6246 additions and 1705 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.beta.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>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -13,15 +13,19 @@
"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",
"ethers": "^6.7.1",
"flexsearch": "^0.7.43",
"lru-cache": "^11.0.2",
"luxon": "^3.4.4",
"pinia": "2.1.6",
"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",
@@ -30,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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

BIN
public/howitworks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1487.72 264.73"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#35d721;}</style></defs><title>Asset 13</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_2-2" data-name="Layer 2"><path class="cls-1" d="M305.33,264.49c-37.72,0-56.58-20.37-56.58-60.73v-72c0-40.37,18.86-60.74,56.58-60.74h62.25V37.6L396.25.05V264.49ZM367.58,100.4H306.47C286.85,100.4,277,111,277,132.84V203c0,21.51,9.81,32.45,29.43,32.45h61.11Z"/><path class="cls-1" d="M472.26,264.59c-37.72,0-56.58-20.37-56.58-60.74v-72c0-40.36,18.86-60.73,56.58-60.73h34.33c38.1,0,57,20.37,57,60.73v47.91H443.22V203.1c0,21.5,9.8,32.44,29,32.44h91.29l-18.75,29Zm64.13-133.54c0-21.51-9.81-32.45-29.42-32.45H472.26c-19.24,0-29,10.94-29,32.45v23.76h93.17Z"/><path class="cls-1" d="M789,264.73c-50.17,0-62.62-16.22-62.62-65.64V136.85c0-51.68,15.09-65.64,62.62-65.64H816.2c49.8,0,62.62,16.6,62.62,65.64v62.24c0,43.76-12.45,65.64-51.68,65.64ZM850.53,132c0-20-6.79-31.31-24.52-31.31H778.86c-16.22,0-24.15,8.67-24.15,26.78v77c0,19.61,6.42,31.31,24.15,31.31H826c16.22,0,24.52-9.06,24.52-26.79Z"/><path class="cls-1" d="M927.06,263.88l-.4-131.68c0-21.88,9.81-32.44,29.42-32.44h61.12V70.34H955c-37.72,0-56.58,20.37-56.58,60.73l-.12,132.81Z"/><path class="cls-1" d="M1397.58,264.45l-.4-131.67c0-21.88,9.81-32.45,29.42-32.45h61.12V70.91h-62.25c-37.72,0-56.58,20.37-56.58,60.73l-.12,132.81Z"/><polygon class="cls-1" points="721.45 71.07 689.08 71.07 644.02 142.86 598.4 71.07 567.54 71.07 628.31 167.89 567.54 264.71 598.4 264.71 644.02 192.92 689.08 264.71 721.45 264.71 659.93 167.89 721.45 71.07"/><path class="cls-1" d="M1258.05,264.44c-37.72,0-56.58-20.37-56.58-60.73V131.65c0-40.36,18.86-60.73,56.58-60.73h34.33c38.1,0,57,20.37,57,60.73v47.91H1229V203c0,21.5,9.81,32.44,29,32.44h91.29v29.05Zm64.13-133.54c0-21.5-9.81-32.44-29.42-32.44h-34.71c-19.24,0-29,10.94-29,32.44v23.77h93.18Z"/><path class="cls-1" d="M1091.12,264.44c-37.72,0-56.58-20.37-56.58-60.73V131.65c0-40.36,18.86-60.73,56.58-60.73h62.25V37.55L1182,0V264.44Zm62.25-164.1h-61.12c-19.61,0-29.42,10.57-29.42,32.45V203c0,21.5,9.81,32.44,29.42,32.44h61.12Z"/><polygon class="cls-2" points="99.15 0.05 0 129.65 53.91 129.65 53.78 248.92 145.01 129.65 197.74 129.65 99.15 0.05"/><polygon class="cls-2" points="95.07 264.49 145.02 199.22 144.98 264.49 95.07 264.49"/><polygon class="cls-2" points="53.79 264.49 145.04 145.2 145.04 156.67 62.55 264.49 53.79 264.49"/><polygon class="cls-2" points="74.39 264.49 145.02 172.16 145.02 183.62 83.16 264.49 74.39 264.49"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1487.72 264.73"><defs><style>.cls-1{fill:#0f0f0f;}.cls-2{fill:#35d721;}</style></defs><title>Asset 12</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_2-2" data-name="Layer 2"><path class="cls-1" d="M305.33,264.49c-37.72,0-56.58-20.37-56.58-60.73v-72c0-40.37,18.86-60.74,56.58-60.74h62.25V37.6L396.25.05V264.49ZM367.58,100.4H306.47C286.85,100.4,277,111,277,132.84V203c0,21.51,9.81,32.45,29.43,32.45h61.11Z"/><path class="cls-1" d="M472.26,264.59c-37.72,0-56.58-20.37-56.58-60.74v-72c0-40.36,18.86-60.73,56.58-60.73h34.33c38.1,0,57,20.37,57,60.73v47.91H443.22V203.1c0,21.5,9.8,32.44,29,32.44h91.29l-18.75,29Zm64.13-133.54c0-21.51-9.81-32.45-29.42-32.45H472.26c-19.24,0-29,10.94-29,32.45v23.76h93.17Z"/><path class="cls-1" d="M789,264.73c-50.17,0-62.62-16.22-62.62-65.64V136.85c0-51.68,15.09-65.64,62.62-65.64H816.2c49.8,0,62.62,16.6,62.62,65.64v62.24c0,43.76-12.45,65.64-51.68,65.64ZM850.53,132c0-20-6.79-31.31-24.52-31.31H778.86c-16.22,0-24.15,8.67-24.15,26.78v77c0,19.61,6.42,31.31,24.15,31.31H826c16.22,0,24.52-9.06,24.52-26.79Z"/><path class="cls-1" d="M927.06,263.88l-.4-131.68c0-21.88,9.81-32.44,29.42-32.44h61.12V70.34H955c-37.72,0-56.58,20.37-56.58,60.73l-.12,132.81Z"/><path class="cls-1" d="M1397.58,264.45l-.4-131.67c0-21.88,9.81-32.45,29.42-32.45h61.12V70.91h-62.25c-37.72,0-56.58,20.37-56.58,60.73l-.12,132.81Z"/><polygon class="cls-1" points="721.45 71.07 689.08 71.07 644.02 142.86 598.4 71.07 567.54 71.07 628.31 167.89 567.54 264.71 598.4 264.71 644.02 192.92 689.08 264.71 721.45 264.71 659.93 167.89 721.45 71.07"/><path class="cls-1" d="M1258.05,264.44c-37.72,0-56.58-20.37-56.58-60.73V131.65c0-40.36,18.86-60.73,56.58-60.73h34.33c38.1,0,57,20.37,57,60.73v47.91H1229V203c0,21.5,9.81,32.44,29,32.44h91.29v29.05Zm64.13-133.54c0-21.5-9.81-32.44-29.42-32.44h-34.71c-19.24,0-29,10.94-29,32.44v23.77h93.18Z"/><path class="cls-1" d="M1091.12,264.44c-37.72,0-56.58-20.37-56.58-60.73V131.65c0-40.36,18.86-60.73,56.58-60.73h62.25V37.55L1182,0V264.44Zm62.25-164.1h-61.12c-19.61,0-29.42,10.57-29.42,32.45V203c0,21.5,9.81,32.44,29.42,32.44h61.12Z"/><polygon class="cls-2" points="99.15 0.05 0 129.65 53.91 129.65 53.78 248.92 145.01 129.65 197.74 129.65 99.15 0.05"/><polygon class="cls-2" points="95.07 264.49 145.02 199.22 144.98 264.49 95.07 264.49"/><polygon class="cls-2" points="53.79 264.49 145.04 145.2 145.04 156.67 62.55 264.49 53.79 264.49"/><polygon class="cls-2" points="74.39 264.49 145.02 172.16 145.02 183.62 83.16 264.49 74.39 264.49"/></g></g></svg>

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,8 +1,14 @@
<template>
<router-view/>
<support-chat/>
<welcome-dialog v-model="prefs.newbie"/>
</template>
<script setup>
import SupportChat from "@/components/SupportChat.vue";
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/')
@@ -31,7 +31,7 @@ export async function queryHelperContract(helper, provider) {
// use newContract(addr, 'IVaultImpl', provider, 'IVault') to get the ABI from IVault.sol/IVaultImpl.json
export async function newContract(addr, name, provider) {
const abi = await abiCache.get(name)
console.log(`${name} ABI`, abi)
// console.log(`${name} ABI`, abi)
return new ethers.Contract(addr, abi, provider)
}

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,5 +1,6 @@
import {uint32max, uint64max} from "@/misc.js";
import {decodeIEE754, encodeIEE754} from "@/common.js";
import {encodeIEE754} from "@/common.js";
export const MAX_FRACTION = 65535;
export const NO_CONDITIONAL_ORDER = uint64max;
@@ -7,6 +8,10 @@ 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,6 +144,7 @@ export function parseElaboratedOrderStatus(chainId, status) {
export function parseOrderStatus(chainId, status) {
// console.log('parseOrderStatus', status)
let [
order,
fillFeeHalfBps,
@@ -151,31 +159,35 @@ export function parseOrderStatus(chainId, status) {
order = parseOrder(order)
filledIn = BigInt(filledIn)
filledOut = BigInt(filledOut)
trancheStatus = trancheStatus.map((obj)=>parseTrancheStatus(obj))
const filled = order.amountIsInput ? filledIn : filledOut
trancheStatus = trancheStatus.map((obj)=>parseTrancheStatus(obj, order.amountIsInput))
const result = {
chainId, order, fillFeeHalfBps, state, startTime, startPrice, ocoGroup,
filledIn, filledOut, trancheStatus,
filledIn, filledOut, filled, trancheStatus,
};
console.log('SwapOrderStatus', result)
// console.log('SwapOrderStatus', result)
return result
}
function parseFill(obj) {
let [tx, time, filledIn, filledOut, fee] = obj
// time = new Date(time * 1000)
filledIn = BigInt(filledIn)
filledOut = BigInt(filledOut)
const filled = obj.amountIsInput ? filledIn : filledOut
fee = BigInt(fee)
return {tx, time, filledIn, filledOut, fee}
return {tx, time, filledIn, filledOut, filled, fee}
}
function parseTrancheStatus(obj) {
function parseTrancheStatus(obj, amountIsInput) {
let [filledIn, filledOut, activationTime, startTime, endTime, rawFills,] = obj
filledIn = BigInt(filledIn)
filledOut = BigInt(filledOut)
const fills = []
for (const fill of rawFills)
fills.push(parseFill(fill))
return {filledIn, filledOut, activationTime, startTime, endTime, fills}
fills.push(parseFill(fill, amountIsInput))
const filled = amountIsInput ? filledIn : filledOut
return {filledIn, filledOut, filled, activationTime, startTime, endTime, fills}
}
export function parseOrder(order) {
@@ -245,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
}
@@ -61,21 +70,28 @@ export async function addExtraToken(chainId, addr) {
}
else {
if( provider===null ) {
console.log('warning: token lookup cancelled due to null provider', addr)
console.warn('warning: token lookup cancelled due to null provider', addr)
resolve(null)
}
else {
const token = await newContract(addr, 'IERC20Metadata', provider)
Promise.all( [token.name(), token.symbol(), token.decimals()] ).then((name,symbol,decimals)=>{
info = {
a: addr,
n: name,
s: symbol,
d: decimals,
for( let tries=1; tries<=5; tries++ ) {
try {
const token = await newContract(addr, 'IERC20Metadata', provider)
const [name, symbol, decimals] = await Promise.all([token.name(), token.symbol(), token.decimals()])
info = {
a: addr,
n: name,
s: symbol,
d: decimals,
}
s.addToken(chainId, info)
resolve(info)
break
}
s.addToken(chainId, info)
resolve(info)
})
catch (e) {
console.warn(`Could not lookup token ${addr}`, e)
}
}
}
}
})

View File

@@ -0,0 +1,270 @@
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 {placementFee} from "@/fees.js";
import {router} from "@/router/router.js";
export class Transaction {
constructor(chainId, type) {
this.id = uuid()
this.type = type
this.state = TransactionState.Created
this.tx = null
this.chainId = chainId
this.owner = null
this.vault = null
this.error = null
}
submit() {
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
}
this.owner = owner
this.vault = vault
this.send().catch(this.catchSend.bind(this))
this.state = TransactionState.Proposed
}
async createTx(vaultContract) {
throw Error('unimplemented')
}
signed(tx) {
this.tx = tx
this.state = TransactionState.Signed
}
rejected() {
this.tx = null
this.chainId = null
this.owner = null
this.vault = null
this.end(TransactionState.Rejected)
console.log('transaction rejected', this.id)
}
failed(e) {
this.error = e
this.end(TransactionState.Error)
console.log('transaction failed', this.id, e)
}
mined(receipt) {
this.receipt = receipt
this.end(TransactionState.Mined)
console.log('mined transaction', this.id, receipt)
}
isOpen() {
return this.state >= TransactionState.Rejected
}
isClosed() {
return this.state < TransactionState.Rejected
}
end(state) {
this.state = state
useWalletStore().transaction = null
}
async send() {
console.log('sendTransaction', this)
try {
await switchChain(this.chainId)
} catch (e) {
if (e.code === 4001) {
this.rejected()
return null
} else {
this.failed(e)
return null
}
}
let signer
try {
signer = await provider.getSigner();
} catch (e) {
// {
// "code": -32002,
// "message": "Already processing eth_requestAccounts. Please wait."
// }
this.rejected()
return null
}
let contract
try {
contract = await vaultContract(this.vault, signer)
} catch (e) {
this.failed('vault contract was null while sending order transaction')
return null
}
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
}
catchSend(e) {
this.error = e
if (e.info?.error?.code === 4001) {
console.log(`wallet refused transaction`, this.id)
this.rejected()
} else {
this.failed(e)
}
}
}
export class PlaceOrderTransaction extends Transaction {
constructor(chainId, order) {
super(chainId, TransactionType.PlaceOrder)
this.order = order
this.placementTime = Date.now()/1000
this.fee = null // dexorder place and gas fee total
}
async createTx(vaultContract) {
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)})
}
end(state) {
super.end(state)
if (state === TransactionState.Mined) {
useChartOrderStore().resetOrders()
// noinspection JSIgnoredPromiseFromCall
router.push({name: 'Status'})
}
}
}
export class CancelOrderTransaction extends Transaction {
constructor(chainId, index) {
super(chainId, TransactionType.CancelOrder)
this.index = index
}
async createTx(vaultContract) {
return await vaultContract.cancelDexorder(this.index)
}
}
export class CancelAllTransaction extends Transaction {
constructor(chainId, vault) {
super(chainId, TransactionType.CancelAll)
this.vault = vault
}
async createTx(vaultContract) {
return await vaultContract.cancelAllDexorders()
}
}
export class WithdrawTransaction extends Transaction {
constructor(chainId, vault, token, amount) {
super(chainId, TransactionType.Withdraw)
this.token = token
this.amount = amount
this.vault = vault
}
async createTx(vaultContract) {
return await vaultContract['withdraw(address,uint256)'](this.token.a, this.amount)
}
}
export class WithdrawNativeTransaction extends Transaction {
constructor(chainId, vault, amount) {
super(chainId, TransactionType.WithdrawNative)
this.amount = amount
this.vault = vault
}
async createTx(vaultContract) {
return await vaultContract['withdraw(uint256)'](this.amount)
}
}
export class WrapTransaction extends Transaction {
constructor(chainId, vault, amount) {
super(chainId, TransactionType.Wrap)
this.vault = vault
this.amount = amount
}
async createTx(vaultContract) {
return await vaultContract.wrap(this.amount)
}
}
export class UnwrapTransaction extends Transaction {
constructor(chainId, vault, amount) {
super(chainId, TransactionType.Unwrap)
this.amount = amount
}
async createTx(vaultContract) {
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,21 +1,21 @@
import {provider, setProvider} from "@/blockchain/provider.js";
import {BrowserProvider, ethers} from "ethers";
import {useStore} from "@/store/store";
import {socket} from "@/socket.js";
import {SingletonCoroutine, timestamp, uuid} 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";
export let provider = null
import {TransactionState, TransactionType} from "@/blockchain/transactionDecl.js";
import {track} from "@/track.js";
import {socket} from "@/socket.js";
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)
// Pending Order Format
// OLD Pending Order Format
// {
// chainId: 31337, // must never be null, even if no wallet plugin exists. chosen by app, not wallet.
// placementTime: Date.now(),
@@ -26,8 +26,30 @@ export const useWalletStore = defineStore('wallet', ()=>{
// }
const pendingOrders = ref([])
// NEW Format is a single Transaction class
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,
chainId, pendingOrders, transaction,
}
})
@@ -35,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 ) {
@@ -44,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 {
@@ -68,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)
}
}
@@ -94,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) {
@@ -148,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
}
@@ -165,7 +201,8 @@ function discoverVaults(owner) {
doDiscoverVaults.invoke(owner)
}
const doDiscoverVaults = new SingletonCoroutine(_discoverVaults, 50, false)
const doDiscoverVaults = new SingletonCoroutine(_discoverVaults, 50)
async function _discoverVaults(owner) {
const result = []
const versions = []
@@ -182,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
@@ -194,21 +230,23 @@ 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().pendingOrders.length ) {
if( useWalletStore().transaction ) {
const num = 0 // todo multiple vaults
if (result.length)
flushOrders(result[0])
flushWalletTransactions(s.chainId, owner, num, result[0])
else
ensureVault2(s.chainId, owner, 0)
ensureVault2(s.chainId, owner, num)
}
}
}
@@ -248,7 +286,7 @@ async function doEnsureVault(chainId, owner, num) {
if (s.vaults.length <= num)
await _discoverVaults(owner)
if( s.vaults[num] )
flushOrders(s.vaults[num])
flushWalletTransactions(chainId, owner, num, s.vaults[num])
else {
console.log(`requesting vault ${owner} ${num}`)
socket.emit('ensureVault', chainId, owner, num)
@@ -258,53 +296,6 @@ async function doEnsureVault(chainId, owner, num) {
const ensureVaultRoutine = new SingletonCoroutine(doEnsureVault, 100)
export const PendingOrderState = {
Submitted: -100, // user clicked Place Order but the tx isn't sent to the wallet yet
Signing: 0, // tx is sent to the wallet
Rejected: -101, // user refused to sign the tx
Sent: -102, // tx is awaiting blockchain mining
}
// 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))'
export 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)
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 async function pendOrder(order, fee=null) {
const s = useStore()
const pend = {
id: uuid(),
chainId: s.chainId,
placementTime: Date.now()/1000,
fee: fee, // dexorder place and gas fee total
vault: s.vaults.length ? s.vaults[0] : null,
state: PendingOrderState.Submitted,
order
};
useWalletStore().pendingOrders.splice(0,0, pend)
console.log('pended order', pend.id, JSON.stringify(order))
ensureVault()
}
export async function cancelOrder(vault, orderIndex) {
console.log('cancel order', vault, orderIndex)
pendTransaction(async (signer)=> {
@@ -317,27 +308,75 @@ export async function cancelOrder(vault, orderIndex) {
})
}
export async function cancelAll(vault) {
pendTransaction(async (signer)=> {
const contract = await vaultContract(vault, signer)
if( contract === null ) {
console.error('vault contract was null while canceling order', vault)
return null
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()
}
return await contract.cancelAllDexorders()
})
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(vault) {
const transactionProgressor = new SingletonCoroutine(progressTransactions, 10)
let progressionInvoker = null
export function flushWalletTransactions(chainId, owner, num, vault) {
const ws = useWalletStore();
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
if (pend.state === PendingOrderState.Submitted) {
console.log('flushing order', pend.id)
pendOrderAsTransaction(pend)
pend.state = PendingOrderState.Signing
setPendState(pend, PendingOrderState.Signing)
needsFlush = true
}
}
@@ -363,7 +402,7 @@ function pendOrderAsTransaction(pend) {
catch (e) {
if(e.code===4001) {
console.log('user refused chain switch')
pend.state = PendingOrderState.Rejected
setPendState(pend, PendingOrderState.Rejected)
return null
}
else {
@@ -378,7 +417,7 @@ function pendOrderAsTransaction(pend) {
console.log('placing order', pend.id, pend.fee, pend.order)
const tx = await contract.placeDexorder(pend.order, {value:pend.fee})
pend.tx = tx
pend.state = PendingOrderState.Sent
setPendState(pend, PendingOrderState.Sent)
console.log(`order ${pend.id} sent transaction`, tx)
tx.wait().then((txReceipt)=>{
console.log('mined order', pend.id, txReceipt)
@@ -391,7 +430,7 @@ function pendOrderAsTransaction(pend) {
(e) => {
if( e.info?.error?.code === 4001 ) {
console.log(`wallet refused order`, pend.id)
pend.state = PendingOrderState.Rejected
setPendState(pend, PendingOrderState.Rejected)
return true // returning true means we handled the error. any other return value will dump to console.
}
})
@@ -399,6 +438,7 @@ function pendOrderAsTransaction(pend) {
export function pendTransaction(sender, errHandler) {
console.log('pendTransaction')
const s = useStore()
s.transactionSenders.push([sender,errHandler])
flushTransactions()
@@ -413,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 {
@@ -464,18 +523,18 @@ function doSendTransaction(sender, signer, errHandler) {
export async function detectUpgrade() {
if (!provider) {
console.log('no provider!')
return 0
return null
}
const s = useStore()
if (!s.vault) {
console.log('no vault logged in')
return 0
return null
}
const info = version.chainInfo[s.chainId]
if (!info) {
console.log(`couldn't get chainInfo for ${s.chainId}`)
return 0
return null
}
try {
console.log('factory', info.factory)
@@ -497,7 +556,7 @@ export async function detectUpgrade() {
catch (e) {
console.log('ignorable error while querying for an upgrade', e)
}
return 0
return null
}
@@ -535,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",
@@ -577,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

@@ -52,4 +52,8 @@ export function dirtyItems(a, b) {
for (const k of dirtyKeys(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,14 +25,13 @@ export function removeSymbolChangedCallback(cb) {
}
function symbolChanged(symbol) {
if (symbol===null)
co.selectedSymbol = null
else {
const info = lookupSymbol(symbol.ticker)
symbolChangedCbs.forEach((cb) => cb(info))
co.selectedSymbol = info
}
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)
}
@@ -56,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
}
@@ -82,68 +92,100 @@ 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
// https://www.tradingview.com/charting-library-docs/latest/api/interfaces/Charting_Library.ChartingLibraryWidgetOptions
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,
}
});
// debug dump all events
@@ -154,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))
@@ -184,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')
}
@@ -229,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')
}
@@ -294,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)
@@ -344,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) {
@@ -430,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,7 +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} from "@/misc.js";
import {timestamp, withTimeout} from "@/common.js";
import {erc20Contract} from "@/blockchain/contract.js";
import {track} from "@/track.js";
const DEBUG_LOGGING = false
const log = DEBUG_LOGGING ? console.log : ()=>{}
@@ -19,6 +23,7 @@ const log = DEBUG_LOGGING ? console.log : ()=>{}
const quoteSymbols = [
'USDT',
'USDC',
'USDC.e',
'TUSD',
'GUSD',
'BUSD',
@@ -59,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
@@ -106,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
@@ -118,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)
@@ -129,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])
}
@@ -240,7 +252,6 @@ function invertTicker(ticker) {
}
export function lookupSymbol(ticker) { // lookup by ticker which is "0xbaseAddress/0xquoteAddress"
// todo tim lookup default base/quote pool
const symbols = getAllSymbols();
if (!(ticker in symbols)) {
// check the inverted symbol
@@ -324,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');
@@ -348,6 +367,8 @@ export const DataFeed = {
result.push(_symbols[ticker])
seen[ticker] = true
}
if (userInput.length>=3)
track('search', {search_term: userInput})
onResultReadyCallback(result);
},
@@ -368,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 = {
@@ -405,13 +463,13 @@ export const DataFeed = {
// volume_precision: 2,
data_status: 'streaming',
};
log('[resolveSymbol]: Symbol resolved', symbolName);
// console.log('[resolveSymbol]: Symbol resolved', symbolName);
onSymbolResolvedCallback(symbolInfo)
},
async getBars(symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) {
const { from, to, firstDataRequest } = periodParams;
log('[getBars]: Method call', symbolInfo, resolution, from, to);
log('[getBars]: Method call', symbolInfo, resolution, new Date(from*1000), new Date(to*1000));
try {
// todo need to consider the selected fee tier
await getAllSymbols()
@@ -683,4 +741,4 @@ export const DataFeed = {
let _rolloverBumper = null
let defaultSymbol = null
export let defaultSymbol = null

View File

@@ -1,5 +1,6 @@
import {useStore} from "@/store/store.js";
import {ohlcStart} from "@/charts/chart-misc.js";
import {LRUCache} from "lru-cache";
// support for Dexorder OHLC data files
@@ -43,19 +44,36 @@ function singleFile(resName) {
}
function nextDay(timestamp) {
function addDay(timestamp) {
const date = new Date(timestamp*1000)
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1) / 1000
}
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))
return result
}
function addYear(timestamp) {
const date = new Date(timestamp*1000)
return Date.UTC(date.getUTCFullYear() + 1, date.getUTCMonth(), date.getUTCDate()) / 1000
}
function nextDay(timestamp) {
const date = new Date(timestamp*1000)
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1, 0, 0, 0) / 1000
}
function nextMonth(timestamp) {
const date = new Date(timestamp*1000)
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()) / 1000
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1, 0, 0, 0) / 1000
}
function nextYear(timestamp) {
const date = new Date(timestamp*1000)
return Date.UTC(date.getUTCFullYear() + 1, date.getUTCMonth(), date.getUTCDate()) / 1000
return Date.UTC(date.getUTCFullYear() + 1, 1, 1, 0, 0, 0) / 1000
}
@@ -66,21 +84,21 @@ function never(_timestamp) {
// noinspection PointlessArithmeticExpressionJS
const resolutions = [
{ seconds: 1 * 60, name: '1m', tvRes: '1', filename: dailyFile( '1m'), nextStart: nextDay, },
{ seconds: 3 * 60, name: '3m', tvRes: '3', filename: dailyFile( '3m'), nextStart: nextDay, },
{ seconds: 5 * 60, name: '5m', tvRes: '5', filename: dailyFile( '5m'), nextStart: nextDay, },
{ seconds: 10 * 60, name: '10m', tvRes: '10', filename: dailyFile('10m'), nextStart: nextDay, },
{ seconds: 15 * 60, name: '15m', tvRes: '15', filename: dailyFile('15m'), nextStart: nextDay, },
{ seconds: 30 * 60, name: '30m', tvRes: '30', filename: dailyFile('30m'), nextStart: nextDay, },
{ seconds: 60 * 60, name: '1H', tvRes: '60', filename: monthlyFile( '1H'), nextStart: nextMonth, },
{ seconds: 120 * 60, name: '2H', tvRes: '120', filename: monthlyFile( '2H'), nextStart: nextMonth, },
{ seconds: 240 * 60, name: '4H', tvRes: '240', filename: monthlyFile( '4H'), nextStart: nextMonth, },
{ seconds: 480 * 60, name: '8H', tvRes: '480', filename: monthlyFile( '8H'), nextStart: nextMonth, },
{ seconds: 720 * 60, name: '12H', tvRes: '720', filename: monthlyFile('12H'), nextStart: nextMonth, },
{ seconds: 1440 * 60, name: '1D', tvRes: '1D', filename: yearlyFile( '1D'), nextStart: nextYear, },
{ seconds: 2880 * 60, name: '2D', tvRes: '2D', filename: yearlyFile( '2D'), nextStart: nextYear, },
{ seconds: 4320 * 60, name: '3D', tvRes: '3D', filename: yearlyFile( '3D'), nextStart: nextYear, },
{ seconds: 10080 * 60, name: '1W', tvRes: '1W', filename: singleFile( '1W'), nextStart: never, },
{ seconds: 1 * 60, name: '1m', tvRes: '1', filename: dailyFile( '1m'), add: addDay, nextStart: nextDay, },
{ seconds: 3 * 60, name: '3m', tvRes: '3', filename: dailyFile( '3m'), add: addDay, nextStart: nextDay, },
{ seconds: 5 * 60, name: '5m', tvRes: '5', filename: dailyFile( '5m'), add: addDay, nextStart: nextDay, },
{ seconds: 10 * 60, name: '10m', tvRes: '10', filename: dailyFile('10m'), add: addDay, nextStart: nextDay, },
{ seconds: 15 * 60, name: '15m', tvRes: '15', filename: dailyFile('15m'), add: addDay, nextStart: nextDay, },
{ seconds: 30 * 60, name: '30m', tvRes: '30', filename: dailyFile('30m'), add: addDay, nextStart: nextDay, },
{ seconds: 60 * 60, name: '1H', tvRes: '60', filename: monthlyFile( '1H'), add: addMonth, nextStart: nextMonth, },
{ seconds: 120 * 60, name: '2H', tvRes: '120', filename: monthlyFile( '2H'), add: addMonth, nextStart: nextMonth, },
{ seconds: 240 * 60, name: '4H', tvRes: '240', filename: monthlyFile( '4H'), add: addMonth, nextStart: nextMonth, },
{ seconds: 480 * 60, name: '8H', tvRes: '480', filename: monthlyFile( '8H'), add: addMonth, nextStart: nextMonth, },
{ seconds: 720 * 60, name: '12H', tvRes: '720', filename: monthlyFile('12H'), add: addMonth, nextStart: nextMonth, },
{ seconds: 1440 * 60, name: '1D', tvRes: '1D', filename: yearlyFile( '1D'), add: addYear, nextStart: nextYear, },
{ seconds: 2880 * 60, name: '2D', tvRes: '2D', filename: yearlyFile( '2D'), add: addYear, nextStart: nextYear, },
{ seconds: 4320 * 60, name: '3D', tvRes: '3D', filename: yearlyFile( '3D'), add: addYear, nextStart: nextYear, },
{ seconds: 10080 * 60, name: '1W', tvRes: '1W', filename: singleFile( '1W'), add: (x)=>x, nextStart: never, },
]
const tvResMap = {}
@@ -93,14 +111,21 @@ for (const res of resolutions)
const seriesStarts = {}
const ohlcCache = new LRUCache({max:20,ttl:3600*1000,})
async function getUrl(url) {
let result = ohlcCache[url]
if (result)
return result
try {
const response = await fetch(url)
// console.log('got response', response)
if (response.ok)
return await response.text()
if (response.ok) {
result = await response.text()
ohlcCache[url] = result
return result
}
else
console.error(`could not fetch ${url}: status ${response.statusText}`)
}
@@ -119,12 +144,15 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
let baseUrl
let latest = null // latest time, price
function fill(end, period) {
function fill(end) {
const period = res.seconds
if (latest===null) return
const [latestTime, price] = latest
end = ohlcStart(end, period)
const start = ohlcStart(latestTime+period, period);
for (let now= start; now < end; now += period ) {
const start = ohlcStart(latestTime+period, period)
// if (start<end)
// console.log('filling', latestTime, price, new Date(start*1000), new Date(end*1000))
for (let now=start; now < end; now += period ) {
bars.push({time:now * 1000, open:price, high:price, low:price, close:price})
latest = [now, price]
}
@@ -144,13 +172,13 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
const res = tvResMap[tvRes]
const fetches = []
let start = from
if (!(baseUrl in seriesStarts)) {
try {
// console.log('getting quote', baseUrl+'quote.csv')
const response = await getUrl(baseUrl+'quote.csv')
if (response.length) {
seriesStarts[baseUrl] = parseInt(response.split(',')[0])
const [start,end,price] = response.split(',')
seriesStarts[baseUrl] = parseInt(start)
// console.log(`Series ${baseUrl} starts at ${new Date(start*1000)}`)
}
else {
@@ -161,85 +189,97 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
console.error(e)
}
}
let start = from
if (baseUrl in seriesStarts)
start = Math.max(start, seriesStarts[baseUrl])
for(let now = start; now < to; now = res.nextStart(now)) {
const end = res.nextStart(to)
for(let now = start; now < end; now = res.nextStart(now)) {
const url = baseUrl + res.filename(contract, now);
const prom = getUrl(url)
fetches.push(prom);
}
const responses = await Promise.all(fetches)
for (const response of responses) {
if (response.length) {
let lineNum = 0
response.split('\n').forEach((line) => {
lineNum++
const row = line.split(',')
let time, open, high, low, close=null
switch (row.length) {
case 1:
if (row[0].length !== 0)
console.log(`Warning: weird nonempty row at OHLC line ${lineNum}: "${line}"`)
let finished = false
for( let ri=0; !finished && ri<responses.length; ri++ ) {
let lineNum = 0
const rows = responses[ri].split('\n')
for( let rj=0; rj<rows.length; rj++) {
const line = rows[rj];
if (line.trim().length === 0)
continue
const row = line.split(',')
let time, open, high, low, close=null
switch (row.length) {
case 1:
if (row[0].length !== 0)
console.log(`Warning: weird nonempty row at OHLC line ${lineNum}: "${line}"`)
break
case 2:
time = parseInt(row[0])
let price = parseFloat(row[1])
if (inverted)
price = 1/price
open = latest === null ? price : latest[1]
high = low = close = price
break
case 3:
time = parseInt(row[0])
if (time < from || time >= to)
break
case 2:
time = parseInt(row[0])
if (time < start || time >= to)
break
let price = parseFloat(row[1])
if (inverted)
price = 1/price
open = latest === null ? price : latest[1]
high = low = close = price
break
case 3:
time = parseInt(row[0])
if (time < start || time >= to)
break
open = parseFloat(row[1])
close = parseFloat(row[2])
if (inverted) {
open = 1/open
close = 1/close
}
high = Math.max(open, close)
low = Math.min(open,close)
if (latest!==null)
open = latest[1]
break
case 5:
time = parseInt(row[0])
if (time < start || time >= to)
break
open = parseFloat(row[1])
high = parseFloat(row[2])
low = parseFloat(row[3])
close = parseFloat(row[4])
if (inverted) {
open = 1/open
const h = high
high = 1/low
low = 1/h
close = 1/close
}
break
default:
console.log(`Warning: could not parse line ${lineNum} of OHLC file:\n${line}`)
open = parseFloat(row[1])
close = parseFloat(row[2])
if (inverted) {
open = 1/open
close = 1/close
}
high = Math.max(open, close)
low = Math.min(open,close)
if (latest!==null)
open = latest[1]
break
case 5:
time = parseInt(row[0])
if (time < from || time >= to)
break
open = parseFloat(row[1])
high = parseFloat(row[2])
low = parseFloat(row[3])
close = parseFloat(row[4])
if (inverted) {
open = 1/open
const h = high
high = 1/low
low = 1/h
close = 1/close
}
break
default:
console.log(`Warning: could not parse line ${lineNum} of OHLC file:\n${line}`)
break
}
if (time!==null) {
if (time >= to) {
console.log('time is past end of request:', time)
finished = true
break
}
if (close!==null) {
fill(time, res.seconds)
const bar = {time:time*1000, open, high, low, close};
if ( time >= from ) {
if ( latest !== null && time > latest[0] + res.seconds) {
fill(time)
}
const bar = {time: time * 1000, open, high, low, close};
bars.push(bar)
latest = [time, close]
}
})
// console.log(`processed ${lineNum} lines`)
}
}
// else { console.log('response was empty') }
// console.log(`processed ${lineNum} lines`)
}
fill(to, res.seconds)
// console.log('loadOHLC prefill bars', bars)
fill(to)
// console.log('loadOHLC bars', bars)
return bars
}

View File

@@ -1,8 +1,9 @@
import {DISTANT_FUTURE, DISTANT_PAST} from "@/blockchain/orderlib.js";
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 {timestamp} from "@/misc.js";
import {createShape, deleteShapeId, widget} from "@/charts/chart.js";
import {sideColor} from "@/misc.js";
import {useChartOrderStore} from "@/orderbuild.js";
import {timestamp} from "@/common.js";
export class OrderShapes {
constructor(symbol, orderStatus) {
@@ -16,9 +17,11 @@ export class OrderShapes {
for (const old of this.trancheShapes)
old.delete()
this.status = orderStatus
this.trancheShapes = [];
for (let i = 0; i < orderStatus.trancheStatus.length; i++)
this.trancheShapes.push(new TrancheShapes(this.symbol, this.status, i));
this.trancheShapes = []
const maxAllocation = Math.max(...orderStatus.order.tranches.map((ts)=>ts.fraction)) / MAX_FRACTION
if (orderStatus.trancheStatus !== undefined)
for (let i = 0; i < orderStatus.trancheStatus.length; i++)
this.trancheShapes.push(new TrancheShapes(this.symbol, this.status, i, maxAllocation));
}
show() {for (const t of this.trancheShapes) t.show();}
@@ -28,8 +31,7 @@ export class OrderShapes {
class TrancheShapes {
constructor(symbol, orderStatus, trancheIndex) {
// todo validate base/quote
constructor(symbol, orderStatus, trancheIndex, maxAllocation) {
if (symbol.inverted !== orderStatus.order.inverted) {
console.log('OrderShapes.createLine(): symbol has wrong inverson for this order')
return
@@ -38,14 +40,16 @@ class TrancheShapes {
this.status = orderStatus
this.trancheIndex = trancheIndex
this.tranche = orderStatus.order.tranches[trancheIndex]
this.trancheStatus = orderStatus.trancheStatus[trancheIndex]
this.shapes = []
this.fills = []
this.createShapes();
this.maxAllocation = maxAllocation
this.createShapes()
}
createShapes() {
// todo amounts
const t = this.tranche
// console.log('create tranche shapes', t)
if (t.marketOrder) {
if (t.startTime !== DISTANT_PAST) {
@@ -53,8 +57,10 @@ class TrancheShapes {
}
} else {
// check lines
this.createLine(t.minLine.slope, t.minLine.intercept);
this.createLine(t.maxLine.slope, t.maxLine.intercept);
const amount = this.status.order.amount * BigInt(t.fraction) / BigInt(MAX_FRACTION)
const buy = this.status.order.tokenIn === this.symbol.quote.a
this.createLine(t.minLine.slope, t.minLine.intercept, amount, buy);
this.createLine(t.maxLine.slope, t.maxLine.intercept, amount, !buy);
}
for (const f of this.status.trancheStatus[this.trancheIndex].fills)
this.createFillPoint(f)
@@ -72,67 +78,80 @@ class TrancheShapes {
* 10 ** -(amountIsBase ? this.symbol.base.d : this.symbol.quote.d)
const weight = Number(filledAmount) / Number(this.status.order.amount)
const amountSymbol = amountIsBase ? this.symbol.base.s : this.symbol.quote.s
console.log('fillpoint', buy, filledAmount, this.status.order, this.symbol)
// console.log('fillpoint', buy, filledAmount, this.status.order, this.symbol)
const time = f.time
const out = Number(f.filledOut) / (1-this.status.order.route.fee/1000000)
let price = out / Number(f.filledIn)
if (buy)
price = 1/price
price *= scale
console.log('price', price)
// console.log('price', price)
const channel = buy?'low':'high';
const text = (buy ? 'Buy ' : 'Sell ') + allocationText(weight, amount, amountSymbol, '\n')
const s = createShape(buy?'arrow_up':'arrow_down', {time, price}, {channel,text,lock:true})
console.log('created fill shape at', time, price)
const text = allocationText(buy, weight, amount, amountSymbol, amountIsBase ? null : this.symbol.base.s, 1, '\n')
const color = sideColor(buy);
const options = {channel,text,lock:true,overrides:{color: color, arrowColor: color}};
const s = createShape(buy?'arrow_up':'arrow_down', {time, price}, options)
console.log('created fill shape at', time, price, widget.activeChart().getShapeById(s).getProperties())
this.fills.push(s)
}
createLine(slope, intercept, amount) {
createLine(slope, intercept, amountBigInt, breakout) {
if (intercept === 0 && slope === 0) return
const t = this.tranche
const status = this.status
const symbol = this.symbol
const scale = 10**symbol.decimals;
const buy = status.order.tokenIn === this.symbol.quote.a
amount = Number(amount)
* 10 ** -(buy ? this.symbol.base.d : this.symbol.quote.d)
const decimals = buy ? this.symbol.base.d : this.symbol.quote.d;
amountBigInt = BigInt(amountBigInt)
const amount = Number(amountBigInt) * 10 ** - decimals
const amountSymbol = buy ? this.symbol.base.s : this.symbol.quote.s
const color = buy ? 'green' : 'red'
if (intercept !== 0 || slope !== 0) {
// console.log('tranche line', intercept, slope)
// line active
if (slope === 0) {
let price = intercept
price *= scale
// horizontal line
// console.log('hline', price)
const model = {price, color, maxAllocation: status.order.amount, amount, amountSymbol};
const s = new HLine(model, null, null, null, true)
this.shapes.push(s)
} else {
// diagonal line
let startTime = t.startTime
if (startTime === DISTANT_PAST)
startTime = timestamp() - useChartOrderStore().intervalSecs * 20 // 20 bars ago
let endTime = t.endTime
if (endTime === DISTANT_FUTURE)
endTime = timestamp() // use "now" as the drawing point's time
let startPrice = (intercept + slope * startTime);
let endPrice = (intercept + slope * endTime);
startPrice *= scale
endPrice *= scale
// console.log('dline', startTime, endTime, DISTANT_FUTURE, startPrice, endPrice)
// noinspection EqualityComparisonWithCoercionJS
const model = {
pointA: {time: startTime, price: startPrice},
pointB: {time: endTime, price: endPrice},
extendLeft: t.startTime === DISTANT_PAST,
extendRight: t.endTime === DISTANT_FUTURE,
color,
maxAllocation: status.order.amount, amount, amountSymbol,
};
const s = new DLine(model, null, null, null, true)
this.shapes.push(s)
const color = sideColor(buy)
const maxAllocation = this.maxAllocation
const allocation = t.fraction / MAX_FRACTION
const ts = this.trancheStatus
let sum = 0n
for (let i=0; i<ts.fills.length; i++)
sum += ts.fills[i].filled
const completed = (amountBigInt - sum) < status.order.minFillAmount
const extraText = completed ? '✓' : null
const textLocation = breakout === buy ? 'above' : 'below'
// line active
if (slope === 0) {
let price = intercept
price *= scale
// horizontal line
// console.log('hline', price)
const model = {
price, breakout, color, extraText, textLocation,
allocation, maxAllocation, amount, amountSymbol, buy,
}
const s = new HLine(model, null, null, null, true)
this.shapes.push(s)
} else {
// diagonal line
let startTime = t.startTime
if (startTime === DISTANT_PAST)
startTime = timestamp() - useChartOrderStore().intervalSecs * 20 // 20 bars ago
let endTime = t.endTime
if (endTime === DISTANT_FUTURE)
endTime = timestamp() // use "now" as the drawing point's time
let startPrice = (intercept + slope * startTime);
let endPrice = (intercept + slope * endTime);
startPrice *= scale
endPrice *= scale
// console.log('dline', startTime, endTime, DISTANT_FUTURE, startPrice, endPrice)
// noinspection EqualityComparisonWithCoercionJS
const model = {
pointA: {time: startTime, price: startPrice},
pointB: {time: endTime, price: endPrice},
extendLeft: t.startTime === DISTANT_PAST,
extendRight: t.endTime === DISTANT_FUTURE,
breakout, color, extraText, textLocation,
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 {computeInterceptSlope} from "@/misc.js";
import {defined, toPrecision} from "@/misc.js";
//
@@ -36,28 +36,35 @@ export const ShapeType = {
HLine: {name: 'Horizontal Line', code: 'horizontal_line', drawingProp: 'linetoolhorzline'},
VLine: {name: 'Vertical Line', code: 'vertical_line', drawingProp: 'linetoolvertline'},
PriceRange: {name: 'Price Range', code: 'price_range'},
DateRange: {name: 'Date Range', code: 'date_range', drawingProp: 'linetooldaterange'},
}
export function allocationText(weight, amount, symbol, separator = ' ') {
export function allocationText(buy, weight, amount, baseSymbol, amountSymbol = null, parts = 1, separator = ' ') {
const hasAmount = amount !== null && amount !== undefined && amount > 0
if (hasAmount)
amount = Number(amount)
const hasWeight = weight !== null && weight !== undefined && weight !== 1
if (hasWeight)
weight = Number(weight)
let text = ''
let text = buy === undefined ? '' : buy ? 'Buy ' : 'Sell '
if (hasWeight)
text += `${(weight * 100).toFixed(1)}%`
const hasSymbol = symbol !== null && symbol !== undefined
const hasSymbol = baseSymbol !== null && baseSymbol !== undefined
if (hasAmount && hasSymbol) {
if (hasWeight)
text += separator
text += `${amount.toPrecision(3).toLocaleString('fullwide')} ${symbol}`
if (amountSymbol!==null && amountSymbol!==baseSymbol)
text += `${baseSymbol} worth ${toPrecision(amount,3)} ${amountSymbol}`
else
text += `${toPrecision(amount,3)} ${baseSymbol}`
}
if (parts > 1)
text += separator + `in ${Math.round(parts)} parts`
return text
}
export class Shape {
constructor(type, onModel=null, onDelete=null, props=null, readonly=false, overrides={}) {
@@ -107,9 +114,10 @@ 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 = 'above'
this.model.textLocation = null // defaults to 'above' if not set
// LEAF SUBCLASSES MUST CALL setModel(model) AFTER ALL CONSTRUCTION.
}
@@ -119,20 +127,26 @@ export class Shape {
//
setModel(model) {
if (model.color)
if (defined(model.color))
this.model.color = model.color
if (model.allocation !== null && model.allocation !== undefined)
if (defined(model.allocation))
this.model.allocation = model.allocation
if (model.maxAllocation !== null && model.maxAllocation !== undefined)
if (defined(model.maxAllocation))
this.model.maxAllocation = model.maxAllocation
if (model.amount !== null && model.amount !== undefined)
if (defined(model.amount))
this.model.amount = model.amount
if (model.amountSymbol)
if (defined(model.amountSymbol))
this.model.amountSymbol = model.amountSymbol
if (model.extraText)
if (defined(model.baseSymbol))
this.model.baseSymbol = model.baseSymbol
if (defined(model.extraText))
this.model.extraText = model.extraText
if (model.textLocation)
if (defined(model.breakout))
this.model.breakout = model.breakout
if (defined(model.textLocation))
this.model.textLocation = model.textLocation
if (defined(model.buy))
this.model.buy = model.buy
const newProps = {}
@@ -141,7 +155,7 @@ export class Shape {
newProps.textcolor = color
// line color
if (this.model.allocation && this.model.maxAllocation) {
if (defined(this.model.allocation) && defined(this.model.maxAllocation)) {
const w = this.model.allocation / this.model.maxAllocation
if (!w)
newProps.linecolor = 'rgba(0,0,0,0)'
@@ -157,7 +171,9 @@ export class Shape {
newProps.linecolor = color
// text label
let text = allocationText(this.model.allocation, this.model.amount, this.model.amountSymbol)
let text = allocationText(this.model.buy, this.model.allocation, this.model.amount, this.model.baseSymbol, this.model.amountSymbol)
if (this.model.breakout)
text += ' ' + (this.model.textLocation==='above' ? '▲Breakout▲' : '▼Breakdown▼')
if (this.model.extraText)
text += ' '+this.model.extraText
if (this.debug) text = `${this.id} ` + text
@@ -199,10 +215,13 @@ export class Shape {
const p = this.type.drawingProp
const lc = this.model.lineColor ? this.model.lineColor : this.model.color;
const tc = this.model.textColor ? this.model.textColor : this.model.color;
const tl = this.model.textLocation ? this.model.textLocation : 'above';
if (lc)
o[p+".linecolor"] = lc
if (tc)
o[p+".textcolor"] = tc
if (tl)
o[p+".textlocation"] = tl
return o
}
@@ -312,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)
@@ -369,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
@@ -464,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())
}
}
}
@@ -641,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
@@ -133,3 +136,55 @@ export function abiPath(name) {
}
export function parseFill(obj) {
let [tx, time, filledIn, filledOut, fee] = obj
time = new Date(time * 1000)
filledIn = BigInt(filledIn)
filledOut = BigInt(filledOut)
fee = BigInt(fee)
return {tx, time, filledIn, filledOut, fee}
}
export function timestamp(date = null) {
if (date === null)
date = new Date()
return Math.round(date.getTime() / 1000)
}
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

@@ -0,0 +1,42 @@
<template>
<slot v-if="s.regionApproved"/>
<v-card v-if="s.regionApproved===null" title="Loading..." prepend-icon="mdi-clock-outline"
text="If loading takes more than a second, please refresh your browser or contact support@dexorder.trade"/>
<v-card v-if="s.regionApproved===false" title="Restricted" prepend-icon="mdi-alert" color="yellow-darken-1"
text="Dexorder is not available in your region." rounded="0"/>
</template>
<script setup>
import {useStore} from "@/store/store";
import {useRoute} from "vue-router";
import {socket} from "@/socket.js";
const s = useStore()
let timer = null
const bypass = useRoute().query.approval
function tryApproval() {
console.log('trying region approval')
if (timer!==null) {
clearTimeout(timer)
timer = null
}
if (s.regionApproved===true) {
console.log('region approved')
}
else if (s.regionApproved===null) {
console.log('asking for region approval', bypass)
socket.emit('approveRegion', bypass)
timer = setTimeout(tryApproval, 5000)
}
}
tryApproval()
</script>
<style scoped lang="scss">
</style>

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,35 +0,0 @@
<template>
<slot v-if="s.allowed"/>
<v-card v-if="!s.allowed" rounded="0" title="Dexorder Closed Beta">
<v-card-item><v-text-field v-model="password" label="Beta Password" class="maxw"/></v-card-item>
<v-card-text>
Follow our social media for public release updates!
</v-card-text>
<v-card-item><social/></v-card-item>
</v-card>
</template>
<script setup>
import {useStore} from "@/store/store";
import Social from "@/components/Social.vue";
import {ref, watchEffect} from "vue";
import {hashMessage} from "ethers";
const s = useStore()
const password = ref(null)
const passHash = '0x3e6e96bd824dd0a5e5361853719ef051e939b91f3fc6fd0f685b4c414c7ba89e'
watchEffect(()=>{
if (!password.value) return
const canonical = password.value.replace(/[^a-zA-Z]/g, '').toLowerCase();
const hash = hashMessage(canonical);
if (hash===passHash)
s.allowed = true
})
</script>
<style scoped lang="scss">
</style>

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

@@ -32,7 +32,8 @@ import {applyLinePoints, linePointsValue, useChartOrderStore} from "@/orderbuild
import {newTranche} from "@/blockchain/orderlib.js";
import TimeEntry from "@/components/TimeEntry.vue";
import RoutePrice from "@/components/RoutePrice.vue";
import {timestamp} from "@/misc.js";
import {timestamp} from "@/common.js";
const s = useStore()
const os = useOrderStore()

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

@@ -2,7 +2,8 @@
<v-footer class="text-center d-flex flex-column mb-12" location="bottom">
<social/>
<!-- <div class="text-grey logo" style="font-size: small; margin-bottom: 0;">dexorder</div>-->
<div class="text-grey" style="font-size: x-small">&copy;{{ new Date().getFullYear() }} Dexorder LLC</div>
<!-- <div class="text-grey" style="font-size: x-small">&copy;{{ new Date().getFullYear() }} Dexorder LLC</div>-->
<div class="text-grey" style="font-size: x-small">Dexorder Trading Services Ltd., British Virgin Islands</div>
</v-footer>
</template>

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

@@ -17,12 +17,12 @@
<script setup>
import {useOrderStore, useStore} from "@/store/store";
import {routeInverted} from "@/misc.js";
import RoutePrice from "@/components/RoutePrice.vue";
import {validateAmount, validateRequired} from "@/validate.js";
import {computed} from "vue";
// noinspection ES6UnusedImports
import {vAutoSelect} from "@/misc.js";
import {routeInverted} from "@/misc.js";
const os = useOrderStore()
const props = defineProps({

View File

@@ -9,9 +9,9 @@
<script setup>
import PairPrice from "@/components/PairPrice.vue";
import {computed} from "vue";
import {computed, defineAsyncComponent} from "vue";
import {useStore} from "@/store/store.js";
const PairPrice = defineAsyncComponent(()=>import("@/components/PairPrice.vue"))
const props = defineProps(['base', 'quote', 'm', 'b', 'isBreakout', 'showBtn', 'buy'])
const s = useStore()

View File

@@ -1,18 +1,31 @@
<template>
<span class="d-inline-flex align-center">
<v-icon icon="mdi-arrow-up-bold" color="primary" class="arrow"/>
<span class="logo">dexorder</span>
<beta v-if="showTag"/>
</span>
<div class="d-inline-flex">
<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

@@ -26,9 +26,8 @@
<script setup>
import {useStore} from "@/store/store";
import {computed, ref} from "vue";
import {vaultContract} from "@/blockchain/contract.js"
import {pendTransaction} from "@/blockchain/wallet.js";
import {FixedNumber} from "ethers";
import {WrapTransaction} from "@/blockchain/transaction.js";
const s = useStore()
const props = defineProps(['modelValue', 'vault', 'maxAmount'])
@@ -45,10 +44,7 @@ function wrapNative() {
if( amount === 0n )
return
console.log('pending wrap', valueStr, amount)
pendTransaction(async (signer)=>{
const vault = await vaultContract(vaultAddr, signer)
return await vault.wrap(amount)
})
new WrapTransaction(s.chainId, vaultAddr, amount).submit()
floatAmount.value = 0
emit('update:modelValue', false)
}

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

@@ -1,10 +1,10 @@
<template>
<beta-signin>
<approve-region>
<slot v-if="status===Status.OK" v-bind="$props"/>
<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
@@ -33,16 +33,19 @@
</v-card-actions>
</v-card>
</beta-signin>
<terms-of-service v-if="status===Status.OK"/>
</approve-region>
</template>
<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 BetaSignin from "@/components/BetaSignin.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)
@@ -67,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,14 +24,13 @@ import {useOrderStore, useStore} from "@/store/store";
import PhoneCard from "@/components/PhoneCard.vue";
import Amount from "@/components/Amount.vue"
// noinspection ES6UnusedImports
import {nav, routeInverted, SingletonCoroutine, vAutoSelect} from "@/misc.js";
import {SingletonCoroutine, vAutoSelect} from "@/misc.js";
import {newOrder} from "@/blockchain/orderlib.js";
import {FixedNumber} from "ethers";
import {pendOrder} from "@/blockchain/wallet.js";
import router from "@/router/index.js";
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

@@ -33,10 +33,9 @@
import TokenChoice from "@/components/TokenChoice.vue"
import {useOrderStore, useStore} from "@/store/store";
import RoutePrice from "@/components/RoutePrice.vue";
import {findRoute, routeFinder} from "@/blockchain/route.js";
import {SingletonCoroutine, routeInverted} from "@/misc.js";
import {computed, ref} from "vue";
import {queryHelperContract} from "@/blockchain/contract.js";
import {routeFinder} from "@/blockchain/route.js";
import {computed} from "vue";
import {routeInverted} from "@/misc.js";
const s = useStore()
const os = useOrderStore()

View File

@@ -18,7 +18,6 @@ import {flipInversionPreference, inversionPreference} from "@/misc.js";
const props = defineProps(['value', 'base', 'quote', 'showBtn'])
const s = useStore()
const prefs = usePrefStore()
async function token(obj) {
if (!obj) return null
@@ -39,15 +38,13 @@ function pairPrice(chainId, baseToken, quoteToken, price) {
if (price === null || price === undefined)
return null
const decimals = quoteToken.d - baseToken.d
// console.log('pairPrice', chainId, baseToken, quoteToken, price, decimals)
if (decimals >= 0)
price /= 10 ** decimals
else
price *= 10 ** -decimals
// console.log('adjusted pairPrice', price)
if (inversionPreference(chainId, baseToken, quoteToken))
const pref = inversionPreference(chainId, baseToken, quoteToken);
if (pref)
price = 1 / price
// console.log('inverted?', price)
return price
}

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

@@ -16,6 +16,7 @@ function pulse() {
if (props.touch === lastValue) return
lastValue = props.touch
const e = element.value;
if (!e) return
if (!e.classList.contains('pulse')) {
// add class for the first time
e.classList.add('pulse')
@@ -41,10 +42,10 @@ watchEffect(pulse)
@keyframes pulse {
0% {
background-color: gray;
background-color: #888;
}
100% {
background-color: white;
background-color: inherit;
}
}
</style>
</style>

View File

@@ -7,8 +7,8 @@ import {useOrderStore, useStore} from "@/store/store";
import {subPrices, unsubPrices, WIDE_PRICE_FORMAT} from "@/blockchain/prices.js";
import {computed, onBeforeUnmount} from "vue";
import {FixedNumber} from "ethers";
import {routeInverted} from "@/misc.js";
import {subOHLCs} from "@/blockchain/ohlcs.js";
import {routeInverted} from "@/misc.js";
const s = useStore()
const os = useOrderStore()

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,8 +1,8 @@
<template>
<div>
<!-- <v-btn variant="plain" icon="mdi-linkedin" @click="open('https://www.linkedin.com/company/dexorder','dexorder_linkedin')" :size="size"/>-->
<v-btn variant="plain" icon="mdi-discord" @click="open('https://discord.gg/fqp9JXXQyt','dexorder_discord')" :size="size"/>
<v-btn variant="plain" icon="mdi-twitter" @click="open('https://twitter.com/Dexorder_trade','dexorder_twitter')" :size="size"/>
<v-btn variant="plain" icon="mdi-discord" @click="open('https://discord.gg/fqp9JXXQyt','dexorder_discord')" :size="size"/>
<v-btn variant="plain" icon="mdi-github" @click="open('https://github.com/dexorder-trade/contract','dexorder_github')" :size="size"/>
<!-- <v-btn variant="plain" icon="mdi-facebook" @click="open('https://facebook.com','dexorder_facebook')" :size="size"/>-->
<!-- <v-btn variant="plain" icon="mdi-reddit" @click="open('https://reddit.com','dexorder_reddit')" :size="size"/>-->

View File

@@ -5,7 +5,7 @@
<div class="top">
<slot name="top"/>
</div>
<div class="resizer bg-grey-lighten-2" :data-direction="horizontal?'horizontal':'vertical'" ref="resizerElement"></div>
<div class="resizer" :data-direction="horizontal?'horizontal':'vertical'" ref="resizerElement"></div>
<div class="scrollpane">
<slot name="bottom"/>
</div>
@@ -119,14 +119,16 @@ body {
width: 100%;
}
// todo light/dark mode
.resizer[data-direction='horizontal'] {
background-color: #cbd5e0;
background-color: #444;
cursor: ew-resize;
height: 100%;
width: 2px;
}
.resizer[data-direction='vertical'] {
background-color: #444;
cursor: ns-resize;
height: 4px;
width: 100%;
@@ -142,5 +144,7 @@ body {
flex: 1;
width: 100%;
min-height: 5em;
max-height: 100%;
overflow-y: hidden;
}
</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,10 +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>
@@ -88,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="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"
@@ -119,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>
@@ -174,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, PendingOrderState, useWalletStore} from "@/blockchain/wallet.js";
import {timestampString} from "@/misc.js";
import {DISTANT_FUTURE, isOpen, OrderState} from "@/blockchain/orderlib.js";
import {cancelOrder, useWalletStore} from "@/blockchain/wallet.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'))
@@ -214,9 +206,7 @@ watch(selected, async ()=>{
// switch symbol
deleteOrderShapes()
await setSymbol(symbol)
console.log('change symbol completed')
}
console.log('creating order shape', id)
orderShapes[id] = new OrderShapes(symbol, status)
}
}
@@ -228,14 +218,12 @@ function deleteOrderShapes(keepSet={}) {
const keys = [...Object.keys(orderShapes)]
for (const id of keys) {
if (!(id in keepSet)) {
console.log('deleting order shape', id)
orderShapes[id].delete()
delete orderShapes[id]
}
}
}
const orderShapes = {}
onUnmounted(()=>{
@@ -244,7 +232,7 @@ onUnmounted(()=>{
})
const datatableHeaders = [
{title: 'Date', align: 'start', key: 'start'},
{title: 'Date', align: 'start', key: 'startTime'},
{title: 'Input', align: 'end', key: 'input'},
{title: 'Output', align: 'end', key: 'output'},
{title: 'Avg Price', align: 'end', key: 'avg'},
@@ -252,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 = [
@@ -319,6 +317,7 @@ const orders = computed(()=>{
result.push(st)
st.id = `${vault}|${index}`
st.index = parseInt(index)
// st.startTime = timestampString(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

@@ -1,5 +1,8 @@
<template>
</template>
<!-- Crisp -->
<script type="text/javascript">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);})();</script>
<script setup>
if(window.location.hostname !== 'localhost') {
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

@@ -0,0 +1,58 @@
<template>
<v-dialog v-model="show" max-width="500px" persistent>
<tos-card>
<v-card-actions class="justify-space-around mb-8">
<v-btn variant="elevated" @click="decline" :disabled="!acceptable" prepend-icon="mdi-cancel">Decline</v-btn>
<v-btn variant="elevated" color="primary" @click="accept" :disabled="!acceptable" prepend-icon="mdi-check">
Accept
</v-btn>
</v-card-actions>
</tos-card>
</v-dialog>
</template>
<script setup>
// tos = {
// version: 1, // Number
// date: 12345678, // Unix timestamp
// ip: '123.231.132.213', // IP when client accepted
// country: 'AD', // https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 Country where the IP is located
// }
import {usePrefStore} from "@/store/store.js";
import {computed, ref, watch} from "vue";
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
const prefs = usePrefStore()
const ok = computed(()=>{
console.log('check ToS', prefs.acceptedTos)
return prefs.acceptedTos===CURRENT_VERSION
})
const show = computed(()=>!ok.value)
const acceptable = computed(()=>!inFlight.value)
const inFlight = ref(false)
watch(ok, (newVal, oldVal)=> {console.log('check ToS');if(newVal!==oldVal) console.log('ToS accepted', prefs.acceptedTos, prefs.acceptedTos) })
function accept() {
inFlight.value = true
socket.emit('approveTOS', new Date().toISOString(), CURRENT_VERSION, (ok)=>{
console.log('approve TOS', ok)
if (ok) prefs.acceptedTos = CURRENT_VERSION
else console.warn('TOS was not approved')
inFlight.value = false
})
}
function decline() {
// bounce to top-level non-app domain
window.location.href = 'https://dexorder.trade'
}
</script>

View File

@@ -7,10 +7,10 @@
<script setup>
// noinspection ES6UnusedImports
import {routeInverted, SingletonCoroutine, vAutoSelect} from "@/misc.js";
import {SingletonCoroutine, vAutoSelect} from "@/misc.js";
import Order from "@/components/Order.vue";
import LimitPrice from "@/components/LimitPrice.vue";
import {timesliceTranches, applyLimitOld} from "@/orderbuild.js";
import {applyLimitOld, timesliceTranches} from "@/orderbuild.js";
import Tranches from "@/components/Tranches.vue";
import {useOrderStore} from "@/store/store.js";

View File

@@ -81,13 +81,6 @@ function updateValue(v) {
}
</script>
<script>
import {ethers} from "ethers";
import {useStore} from "@/store/store.js";
const s = useStore()
</script>
<style scoped lang="scss">
@use "src/styles/vars" as *;

View File

@@ -15,7 +15,7 @@
<v-list v-if="token">
<v-list-subheader :title="token.s"/>
<v-list-item title="Withdraw" key="withdraw" value="withdraw" prepend-icon="mdi-arrow-down-bold"
@click="()=>onWithdraw(token.a)"/>
@click="()=>onWithdraw(token)"/>
</v-list>
</v-menu>
</td>

710
src/components/TosCard.vue Normal file
View File

@@ -0,0 +1,710 @@
<template>
<v-card>
<v-card-title class="text-center">Dexorder Terms of Service</v-card-title>
<!-- MAKE SURE TO UPDATE THE VERSION VARIABLE AS WELL -->
<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.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.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.
(<b>Dexorder</b>, <b>we</b>, <b>our</b>, or <b>us</b>).
</v-card-text>
<v-card-text>
<b>
BY USING THE DEXORDER SERVICE, YOU REPRESENT THAT (I) YOU ARE NOT LOCATED WITHIN THE UNITED
STATES; AND (II) YOU ARE NOT A PERSON OR ENTITY WHO IS RESIDENT IN, A CITIZEN OF, IS LOCATED IN, IS
INCORPORATED IN, OR HAS A REGISTERED OFFICE IN ANY RESTRICTED TERRITORY, AS DEFINED BELOW (ANY
SUCH PERSON OR ENTITY FROM WITHIN THE UNITED STATES OR A RESTRICTED TERRITORY, IS REFERRED TO
HEREIN AS A RESTRICTED PERSON).
</b>
</v-card-text>
<v-card-text>
WHEN YOU AGREE TO THESE TERMS, YOU ARE AGREEING (WITH LIMITED EXCEPTION) TO RESOLVE ANY DISPUTE
BETWEEN YOU AND DEXORDER THROUGH BINDING, INDIVIDUAL ARBITRATION RATHER THAN IN COURT. PLEASE
REVIEW CAREFULLY SECTION 16 (DISPUTE RESOLUTION) BELOW FOR DETAILS REGARDING ARBITRATION. HOWEVER, IF
YOU ARE A RESIDENT OF A JURISDICTION WHERE APPLICABLE LAW PROHIBITS ARBITRATION OF DISPUTES, THE
AGREEMENT TO ARBITRATE IN SECTION 16 WILL NOT APPLY TO YOU, BUT THE PROVISIONS OF SECTION 15
(GOVERNING LAW) WILL APPLY INSTEAD.
</v-card-text>
<v-card-title>1. Description of Dexorder Service</v-card-title>
<v-card-text>
(a) The Dexorder Service allows you to access an online web application graphical user
interface (the <b>App</b>) which enables you to interact with a protocol consisting of a set of smart contracts
(the
<b>Smart Contracts</b>) and to create and interact with a user controlled smart contract involving digital
assets over
which only you have upgrade authority (a <b>Vault</b> and together with the Smart Contracts, the
<b>Protocol</b>). You
may use your Vault to send signals to, interact with, and initiate actions on third party smart contract
blockchain
protocols (<b>Interactions</b>) operating on decentralized exchanges (e.g., Uniswap) (<b>DEXs</b>). Certain
Interactions
may require threshold parameters to be met and that a third party transmit an oracle related activation signal
(the <b>Activation Signal</b>) to the Vault in order to effectuate your commands (such party being an
<b>Oracle</b>).
Dexorder may in the ordinary course of events be an Oracle (<b>Dexorder Oracle</b>), but bears no obligation nor
promise to do so on an ongoing basis, and you may send such Activation Signal yourself or utilize a third-party
Oracle to do so. Further, Dexorder is not and does not offer a digital wallet, and has no custody or control over
your digital wallet, which is never accessible by Dexorder, and only users, and not Dexorder, may provide or
withdraw tokens to be held by Vault. From time to time, the Services may include making recommendations
with respect to technical changes that only you may accept and implement.
</v-card-text>
<v-card-text>
(b) <b>Interface</b>. The Dexorder Service provides you with access to the Protocol, which is a user
controlled, non-custodial protocol, upgradeable only by your actions and consent, deployed on the blockchains
indicated on our Site, and provides information and interaction capabilities with other blockchain related service
providers. All information provided in connection with your access and use of the Dexorder Service is for
informational purposes only. You should not take, or refrain from taking, any action based on any information
contained on the Dexorder Service or any other information that we make available at any time, including blog
posts, data, articles, links to third-party content, Discord content, news feeds, tutorials, tweets, and videos.
Before you make any financial, legal, technical, or other decisions involving the Dexorder Service, you should
seek independent professional advice from a licensed and qualified individual in the area for which such advice
would be appropriate. Because the Dexorder Service provides information about the Protocol, these Terms also
provide some information about the use of the Protocol. This information is not intended to be comprehensive
or address all aspects of the Protocol.
</v-card-text>
<v-card-text>
(c) <b>Our Relationship</b>. You acknowledge and agree that Dexorder is an online platform
provider and not a financial institution, broker dealer, exchange or money services business. Dexorder does not
direct or control the day-to-day activities of users accessing the Dexorder Service. Neither we nor any affiliated
entity is a party to any transaction on the blockchain network underlying the Protocol; we do not have
possession, custody or control over any cryptoassets appearing on the Dexorder Service or on the Protocol; and
we do not have possession, custody, or control over any users funds or cryptoassets. Further, we do not store,
send, or receive any funds or cryptoassets on your behalf. You understand that when you interact with any
Protocol smart contracts, you retain control over your cryptoassets at all times. You are solely responsible for
evaluating any proposed technical changes and how such changes may alter current or future Interactions.
Furthermore, you understand and acknowledge that only you have absolute and ultimate authority over the
implementation of any such changes and the responsibility therefor. The private key associated with your Vault
is the only private key that can control the cryptoassets in the Vault. You alone are responsible for securing
your
private keys. We do not have access to your private keys. Because the Protocol is non-custodial, we are not
intermediaries, agents, advisors, or custodians, and we do not have a fiduciary relationship or obligation to you
regarding any other decisions or activities that you affect when using the Dexorder Service or interacting with
the Protocol. You acknowledge that we, for the avoidance of doubt, do not have any information regarding any
users, users identities, or services beyond what is available, obtainable publicly via the blockchain, or shared
by
you when you access the Dexorder Service. We are not responsible for any activities you engage in when using
the Dexorder Service, and you should understand the risks associated with cryptoassets, blockchain technology
generally, and the Interface.
</v-card-text>
<v-card-title>
2. Agreement to Terms
</v-card-title>
<v-card-text>
By using our Dexorder Service, you agree to be bound by these Terms. If you dont agree to be bound by these
Terms,
do not use the Dexorder Service.
</v-card-text>
<v-card-title>3. Changes to these Terms or the Dexorder Service</v-card-title>
<v-card-text>
We may update the Terms, including any addendum terms, from time to time in our sole discretion. If we do, well
let
you know by posting the updated Terms on the Site and/or may also send other communications. Its important that
you review the Terms whenever we update them or you use the Dexorder Service. If you continue to use the Dexorder
Service after we have posted updated Terms it means that you accept and agree to the changes. If you dont agree
to
be bound by the changes, you may not use the Dexorder Service anymore. Because our Dexorder Service is evolving
over time we may change or discontinue all or any part of the Dexorder Service, at any time and without notice, at
our
sole discretion.
</v-card-text>
<v-card-title>4. Who May Use the Dexorder Service?</v-card-title>
<v-card-text>
(a) <u>Eligibility</u>. The Dexorder Service is only available to users in certain jurisdictions outside
of the United States and that are at least 18 years old, capable of forming a binding contract with the Dexorder
and not otherwise barred from using the Dexorder Service under Applicable Law. You may not attempt to access
or use the Dexorder Service if you are not permitted to do so (including without limitation if you are a
Restricted
Person).
</v-card-text>
<v-card-text>
(b) <u>Compliance</u>. You certify that you will comply with all Applicable Law when using the
Dexorder Service. You are solely responsible for ensuring that your access and use of the Dexorder Service in
such country, territory, or jurisdiction does not violate any Applicable Laws. You must not use any software or
networking techniques, including use of a virtual private network (<b>VPN</b>) to circumvent or attempt to
circumvent this prohibition. We reserve the right to monitor the locations from which our Dexorder Service is
accessed. Furthermore, we reserve the right, at any time, in our sole discretion, to block access to the Dexorder
Service, in whole or in part, from any geographic location, IP addresses, and unique device identifiers
</v-card-text>
<v-card-title>5. Use of the Dexorder Service</v-card-title>
<v-card-text>
(a) <u>User Representations and Warranties</u>. As a condition to accessing or using the Dexorder
Service, you represent and warrant to Dexorder that:
</v-card-text>
<v-card-text class="ii">
(i) if you are entering into these Terms as an individual, then you are of legal age in
the jurisdiction in which you reside and you have the legal capacity to enter into these Terms and be bound by
them and if you are entering into these Terms as an entity, then you must have the legal authority to accept
these Terms on that entitys behalf, in which case you (except as used in this paragraph) will mean that entity;
</v-card-text>
<v-card-text class="ii">
(ii) you are not in or residing in the United States;
</v-card-text>
<v-card-text class="ii">
(iii) you are not in or residing in Cuba, Iran, North Korea, Syria, Belarus, Russia, and
the Crimea, Luhansk, Donetsk, Zaporizhzhia, and Kherson regions of Ukraine, or any other country or jurisdiction
to which the Cayman Islands, the United Kingdom, United States, the United Nations Security Council, or the
European Union embargoes goods or imposes similar sanctions (collectively, <b>Restricted Territories</b>);
</v-card-text>
<v-card-text class="ii">
(iv) you are not on any sanctions list or equivalent maintained by the Cayman
Islands, the United Kingdom, United States, the United Nations Security Council, or the European Union
(collectively, <b>Sanctions Lists Persons</b>) and you do not intend to transact with any Restricted Person or
Sanctions List Person;
</v-card-text>
<v-card-text class="ii">
(v) you do not, and will not, use VPN software or any other privacy or
anonymization tools or techniques to circumvent, or attempt to circumvent, any restrictions that apply to the
Dexorder Service;
</v-card-text>
<v-card-text class="ii">
(vi) you have obtained all required consents from any individual whose personal
information you transfer to us in connection with your use of the Dexorder Service; and
</v-card-text>
<v-card-text class="ii">
(vii) all information that you provide through the Dexorder Service is current,
complete, true, and accurate and you will maintain the security and confidentiality of your private keys
associated with your public wallet address, passwords, API keys, passwords or other information associated with
your Vault or otherwise, as applicable.
</v-card-text>
<v-card-text class="ii">
(viii) your access to the Dexorder Service is not: (a) prohibited by and does not
otherwise violate or assist you to violate any domestic or foreign law, rule, statute, regulation, by-law, order,
protocol, code, decree, or another directive, requirement, or guideline, published or in force that applies to or
is
otherwise intended to govern or regulate any person, property, transaction, activity, event or other matter,
including any rule, order, judgment, directive or other requirement or guideline issued by any domestic or
foreign federal, provincial or state, municipal, local or other governmental, regulatory, judicial or
administrative
authority having jurisdiction over Dexorder, you, the Site or the Dexorder Service, or as otherwise duly enacted,
enforceable by law, the common law or equity (collectively, <b>Applicable Laws</b>); or (b) contribute to or
facilitate
any illegal activity.
</v-card-text>
<v-card-text>
(b) <u>Limitations</u>. As a condition to accessing or using the Dexorder Service or the Site, you
acknowledge, understand, and agree to the following:
</v-card-text>
<v-card-text class="ii">
(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.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
error
in Interaction execution or a malfunction in the Dexorder Service); (b) equipment malfunctions; (c) periodic
maintenance procedures or repairs that Dexorder or any of its suppliers or contractors may undertake from time
to time; (d) causes beyond Dexorders control or that Dexorder could not reasonably foresee; (e) disruptions and
temporary or permanent unavailability of underlying blockchain infrastructure; (f) unavailability of third-party
service providers or external partners for any reason; or (g) an Activation Signal not being sent.
</v-card-text>
<v-card-text class="ii">
(ii) the Site and the Dexorder Service may evolve, which means Dexorder may apply
changes, replace, or discontinue (temporarily or permanently) the Dexorder Service at any time in its sole
discretion;
</v-card-text>
<v-card-text class="ii">
(iii) Dexorder does not act as an agent for you or any other user of the Site or the Dexorder Service;
</v-card-text>
<v-card-text class="ii">
(iv) you are solely responsible for your use of the Dexorder Service; and
</v-card-text>
<v-card-text class="ii">
(v) we owe no fiduciary duties or liabilities to you or any other party, and that to
the extent any such duties or liabilities may exist at law or in equity, you hereby irrevocably disclaim, waive,
and
eliminate those duties and liabilities.
</v-card-text>
<v-card-title>6. Interactions; Fees</v-card-title>
<v-card-text>
(a) <u>Interactions</u>. In order to effectuate Interactions, you may need to transfer digital assets
(e.g., tokens) to the Vault. You acknowledge that you may use the Dexorder Services to process and cause
Interactions to be operate with an applicable DEX, including without limitation the transfer of digital assets via
the DEX in accordance with the Interaction. For clarity, the Vault is a smart contract automatically controlled by
the blockchain. Dexorder is an interface to that smart contract, and does not offer a digital wallet and has no
custody or control over your digital wallet or any digital assets or cryptocurrency, which is never accessible by
Dexorder.
</v-card-text>
<v-card-text>
(b) <u>Fees</u>.
</v-card-text>
<v-card-text class="ii">
(i) Dexorder charges fees upfront for usage of the Dexorder Services at the time of
user Interactions (<b>Fees</b>). You agree to pay all applicable Fees upfront to Dexorder, in the amounts
communicated or presented to you via the Dexorder Service in connection with usage of the Dexorder Service.
Each party shall be responsible for all Taxes imposed on its income or property.
</v-card-text>
<v-card-text class="ii">
(ii) There may be associated fees in connection with transactions enacted on a
blockchain. All transactions using blockchains require the payment of gas fees, which are essentially transaction
fees paid on every transaction that occurs on the selected blockchain network. We do not collect any such fees.
Please note that accessing the Protocol may result in you incurring gas fees, which are non-refundable, and are
paid by you in all circumstances. You pay all gas fees incurred by you as relating to interacting with the
Protocol.
</v-card-text>
<v-card-text>
(c) <u>Tax Records and Reporting</u>. You are solely responsible for all costs incurred by you in
using the Dexorder Service, and for determining, collecting, reporting, and paying all applicable Taxes that you
may be required by law to collect and remit to any governmental or regulatory agencies. As used herein,
<b>Taxes</b> means the taxes, duties, levies, tariffs, and other charges imposed by any federal, state,
multinational or
local governmental or regulatory authority. We reserve the right to report any activity occurring using the
Dexorder Service to relevant tax authorities as required under Applicable Law. You are solely responsible for
maintaining all relevant Tax records and complying with any reporting requirements you may have as related to
our Dexorder Service. You are further solely responsible for independently maintaining the accuracy of any
record submitted to any tax authority including any information derived from the Dexorder Service.
</v-card-text>
<v-card-text>
(d) Suspensions or Terminations. In addition to the other suspension and termination rights
in these Terms, we may suspend or terminate your access to the Dexorder Service, or any and all Interactions, at
any time in connection with any Interaction or transaction (i) as required by Applicable Law or any governmental
authority, (ii) if we are unable to process or execute an Interaction or transaction after several attempts (as
described in the Execution Policy or otherwise in Dexorders reasonable discretion), or (iii) if we in our sole
and
reasonable discretion determine you are violating the terms of any third-party service provider or these Terms,
including, without limitation, if we reasonably believe any of your representations and warranties may be untrue
or inaccurate or you are violating or have violated any of the geographical restrictions that apply to the
Dexorder Service, and in any case we will not be liable to you for any losses or damages you may suffer as a
result of or in connection with the Dexorder Service being inaccessible to you at any time or for any reason. Such
suspension or termination shall not constitute a breach of these Terms by Dexorder. In accordance with its anti-
money laundering, anti-terrorism, anti-fraud, and other compliance policies and practices, we may impose
limitations and controls on the ability of you or any beneficiary to utilize the Dexorder Service. Such
limitations
may include rejecting transaction requests, freezing funds in any case where Dexorder has such ability, or
otherwise restricting you from using the Dexorder Service, all to the extent of our ability to do so.
</v-card-text>
<v-card-title>7. General Prohibitions and Dexorders Enforcement Rights.</v-card-title>
<v-card-text>
You agree not to do any of the
following:
</v-card-text>
<v-card-text>
(a) Engage in or induce others to engage in any form of unauthorized access, hacking, or
social engineering, including without limitation any distributed denial or service or DDoS attack, of Dexorder,
the
Dexorder Service, or any users of the foregoing;
</v-card-text>
<v-card-text>
(b) Use, display, mirror or frame the Dexorder Service or any individual element within the
Dexorder Service, Dexorders name, any Dexorder trademark, logo or other proprietary information, or the
layout and design of any page or form contained on a page, without Dexorders express written consent;
</v-card-text>
<v-card-text>
(c) Access, tamper with, or use non-public areas of the Dexorder Service, Dexorders
computer systems, or the technical delivery systems of Dexorders providers;
</v-card-text>
<v-card-text>
(d) Attempt to probe, scan or test the vulnerability of any Dexorder system or network or
breach any security or authentication measures;
</v-card-text>
<v-card-text>
(e) Avoid, bypass, remove, deactivate, impair, descramble or otherwise circumvent any
technological measure implemented by Dexorder or any of Dexorders providers or any other third party
(including another user) to protect the Dexorder Service;
</v-card-text>
<v-card-text>
(f) Attempt to access or search the Dexorder Service or download content from the
Dexorder Service using any engine, software, tool, agent, device or mechanism (including spiders, robots,
crawlers, data mining tools or the like) other than the software and/or search agents provided by Dexorder or
other generally available third-party web browsers;
</v-card-text>
<v-card-text>
(g) Use any meta tags or other hidden text or metadata utilizing a Dexorder trademark,
logo, URL or product name without Dexorders express written consent;
</v-card-text>
<v-card-text>
(h) Forge any TCP/IP packet header or any part of the header information in any email or
newsgroup posting, or in any way use the Dexorder Service to send altered, deceptive or false source-identifying
information;
</v-card-text>
<v-card-text>
(i) Attempt to decipher, decompile, disassemble or reverse engineer any of the software
used to provide the Dexorder Service;
</v-card-text>
<v-card-text>
(j) Interfere with, or attempt to interfere with, the access of any user, host or network,
including, without limitation, sending a virus, exploiting any bug, overloading, flooding, spamming, or mail-
bombing the Dexorder Service;
</v-card-text>
<v-card-text>
(k) Use the Dexorder Service for benchmarking or analysis in a manner that could, directly
or indirectly, interfere with, detract from, or otherwise harm the Dexorder Service or DEX;
</v-card-text>
<v-card-text>
(l) Collect or store any personally identifiable information from the Dexorder Service from
other users of the Dexorder Service without their express permission;
</v-card-text>
<v-card-text>
(m) Impersonate or misrepresent your affiliation with any person or entity;
</v-card-text>
<v-card-text>
(n) Create or list any counterfeit items (including digital assets);
</v-card-text>
<v-card-text>
(o) Fabricate in any way any transaction or process related thereto;
</v-card-text>
<v-card-text>
(p) Engage or assist in any activity that violates any law, statute, ordinance, regulation, or
sanctions program, , or that involves proceeds of any unlawful activity (including but not limited to money
laundering, terrorist financing or deliberately engaging in activities designed to adversely affect the
performance
of the Dexorder Service);
</v-card-text>
<v-card-text>
(q) Engage in deceptive or manipulative trading activities;
</v-card-text>
<v-card-text>
(r) Disguise or interfere in any way with the IP address of the computer you are using to
access or use the Dexorder Service or that otherwise prevents us from correctly identifying the IP address of the
computer you are using to access the Dexorder Service;
</v-card-text>
<v-card-text>
(s) Transmit, exchange, or otherwise support the direct or indirect proceeds of criminal or
fraudulent activity;
</v-card-text>
<v-card-text>
(t) Violate any Applicable Law or regulation; or
</v-card-text>
<v-card-text>
(u) Encourage or enable any other individual to do any of the foregoing.
</v-card-text>
<v-card-text>
Dexorder is not obligated to monitor access to or use of the Dexorder Service or to review or edit any content.
However, we have the right to do so for the purpose of operating the Dexorder Service, to ensure compliance with
these Terms and to comply with Applicable Law or other legal requirements. We reserve the right, but are not
obligated, to suspend or terminate access to the Dexorder Service at any time if we believe you are violating
these
Terms. We have the right to investigate violations of these Terms or conduct that affects the Dexorder Service. We
may also consult and cooperate with law enforcement authorities to prosecute users who violate the law.
</v-card-text>
<v-card-title>8. Feedback</v-card-title>
<v-card-text>
We appreciate feedback, comments, ideas, proposals and suggestions for improvements to the Dexorder Service
(<b>Feedback</b>). If you choose to submit Feedback, you agree that we are free to use it (and permit others to
use it)
without any restriction or compensation to you.
</v-card-text>
<v-card-title>9. Links to Third Party Websites or Resources</v-card-title>
<v-card-text>
The Dexorder Service may allow you to access third-party websites, integrations, or other resources, including the
DEX,
services providing Activation Signals, and any bridge between the DEX and any third party protocols (collectively,
<b>Third Party Resources</b>). We provide access only as a convenience and are not responsible for the content,
products
or services on or available from those resources or links displayed on such websites. You acknowledge sole
responsibility for and assume all risk arising from, your use of any third-party resources. Our provision of
access to
Third Party Resources does not constitute approval, endorsement, or control of such Third Party Resource.
</v-card-text>
<v-card-title>10. Termination</v-card-title>
<v-card-text>
We may suspend or terminate your access to and use of the Dexorder Service, at our sole discretion, at any time
and
without notice to you. You acknowledge and agree that we shall have no liability or obligation to you in such
event and
that you will not be entitled to a refund of any amounts that you have already paid to us or any third party, to
the
fullest extent permitted by Applicable Law. Upon any termination, discontinuation, or cancellation of the Dexorder
Service or your account, the following Sections will survive: 6.(d) , 7 , 8 , 10 , 11 , 13 , 14 , 15 , 16 , and 17
.
</v-card-text>
<v-card-title>11. Warranty Disclaimers</v-card-title>
<v-card-text>
THE DEXORDER SERVICE (INCLUDING WITHOUT LIMITATION THE VAULT) AND ANY CONTENT CONTAINED THEREIN, AS
WELL AS THE PROTOCOL, THE DEXORDER ORACLE, AND ANY ASSOCIATED PROTOCOL OR BLOCKCHAIN MESSAGING
FUNCTIONALITY SUCH AS ACTIVATION SIGNALS UNDERLYING THE DEXORDER SERVICE (TOGETHER, THE <b>UTILITIES</b>),
ARE PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND. WITHOUT LIMITING THE FOREGOING, WE EXPLICITLY
DISCLAIM ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET
ENJOYMENT AND NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR USAGE OF
TRADE. WE MAKE NO WARRANTY THAT THE UTILITIES WILL MEET YOUR REQUIREMENTS, BE AVAILABLE ON AN
UNINTERRUPTED, SECURE, OR ERROR-FREE BASIS. WE MAKE NO WARRANTY REGARDING THE QUALITY, ACCURACY,
TIMELINESS, TRUTHFULNESS, COMPLETENESS OR RELIABILITY OF ANY INFORMATION OR CONTENT ON THE UTILITIES.
DEXORDER FURTHER EXPRESSLY DISCLAIMS ALL LIABILITY OR RESPONSIBILITY IN CONNECTION WITH THIRD PARTY
SERVICES. NOTHING HEREIN NOR ANY USE OF THE UTILITIES IN CONNECTION WITH THIRD PARTY SERVICES
CONSTITUTES OUR ENDORSEMENT, RECOMMENDATION OR ANY OTHER AFFILIATION OF OR WITH ANY THIRD PARTY
SERVICES.
</v-card-text>
<v-card-text>
DEXORDER DOES NOT REPRESENT OR WARRANT THAT ANY CONTENT ON THE UTILITIES IS ACCURATE, COMPLETE,
RELIABLE, CURRENT OR ERROR-FREE. WE WILL NOT BE LIABLE FOR ANY LOSS OF ANY KIND FROM ANY ACTION TAKEN
OR TAKEN IN RELIANCE ON MATERIAL OR INFORMATION CONTAINED ON THE UTILITIES. DEXORDER CANNOT AND
DOES NOT REPRESENT OR WARRANT THAT THE UTILITIES, ANY CONTENT THEREIN, OR OUR SERVERS ARE FREE OF
VIRUSES OR OTHER HARMFUL COMPONENTS.
</v-card-text>
<v-card-text>
WE CANNOT GUARANTEE THE SECURITY OF ANY DATA THAT YOU DISCLOSE ONLINE. YOU ACCEPT THE INHERENT
SECURITY RISKS OF PROVIDING INFORMATION AND DEALING ONLINE OVER THE INTERNET AND WILL NOT HOLD US
RESPONSIBLE FOR ANY BREACH OF SECURITY.
</v-card-text>
<v-card-text>
DEXORDER WILL NOT BE RESPONSIBLE OR LIABLE TO YOU FOR ANY LOSS AND TAKES NO RESPONSIBILITY FOR, AND
WILL NOT BE LIABLE TO YOU FOR, ANY USE OF THE UTILITIES, INCLUDING BUT NOT LIMITED TO ANY LOSSES,
DAMAGES OR CLAIMS ARISING FROM: (I) USER ERROR SUCH AS FORGOTTEN PASSWORDS, INCORRECTLY
CONSTRUCTED TRANSACTIONS, EXCEEDING TRANSFER LIMITS OF THIRD PARTY RESOURCES OR THE DEX, OR
MISTYPED WALLET ADDRESSES; (II) SERVER FAILURE OR DATA LOSS; (III) BLOCKCHAIN NETWORKS, CRYPTOCURRENCY
WALLETS, CORRUPT FILES, SOFTWARE ERRORS, OR BUGS; (IV) UNAUTHORIZED ACCESS TO THE UTILITIES; OR (V) ANY
THIRD PARTY UTILITIES, INCLUDING WITHOUT LIMITATION THE USE OF VIRUSES, PHISHING, BRUTEFORCING OR OTHER
MEANS OF ATTACK AGAINST ANY BLOCKCHAIN NETWORK UNDERLYING THE UTILITIES.
</v-card-text>
<v-card-text>
THE UTILITIES MAY INCLUDE THE PLACEMENT OR EXECUTION OF AN ACTIVATION SIGNAL TO A USER VAULT WITH
RESPECT TO USER DEFINED INTERACTIONS (E.G., VIA TRANSMITTING AN ACTIVATION SIGNAL WHICH TRIGGERS
INTERACTION EXECUTION), HOWEVER, ANYONE, INCLUDING YOU OR ANY THIRD PARTY, MAY CAUSE AN INTERACTION
TO BE EXECUTED (SUCH AS BY TRANSMITTING THE APPLICABLE ACTIVATION SIGNAL), AND NOTWITHSTANDING
ANYTHING TO THE CONTRARY IN THESE TERMS, DEXORDER DOES NOT GUARANTEE THE PLACEMENT OR EXECUTION
OR ANY INTERACTION, INCLUDING WITHOUT LIMITATION THAT DEXORDER WILL TRANSMIT ANY PARTICULAR
ACTIVATION SIGNAL TO TRIGGER EXECUTION OF ANY INTERACTION, OR THAT AN INTERACTION WILL OTHERWISE BE
PROPERLY CARRIED OUT PURSUANT TO A VAULT. YOU ACKNOWLEDGE AND AGREE THAT YOU WILL NOT RELY ON THE
UTILITIES TO CARRY OUT PLACEMENTS OR EXECUTIONS OF INTERACTIONS. IF ANY PARTICULAR INTERACTION THAT IS
PLACED IS NOT EXECUTED BY DEXORDER IN A TIMELY MANNER, YOU MAY CAUSE SUCH INTERACTION TO BE
EXECUTED YOURSELF OR BY ENGAGING A THIRD PARTY SERVICE PROVIDER TO DO SO (E.G., BY TRANSMITTING THE
APPLICABLE ACTIVATION SIGNAL YOURSELF OR ENGAGING A THIRD PARTY TO DO SO). YOU ACCEPT THE INHERENT
RISK THAT ANY PARTICULAR INTERACTION MAY NOT BE EXECUTED, INCLUDING WITHOUT LIMITATION DUE TO BAD
ACTORS OR THE MALFUNCTION OF THE UTILITIES, AND DEXORDER HEREBY DISCLAIMS ANY AND ALL LIABILITY AND
RESPONSIBILITY IN CONNECTION WITH THE PLACEMENT OR EXECUTION OF INTERACTIONS AND THE VAULT.
</v-card-text>
<v-card-text>
THE UTILITIES MAY NOT BE AVAILABLE DUE TO ANY NUMBER OF FACTORS INCLUDING, BUT NOT LIMITED TO,
PERIODIC SYSTEM MAINTENANCE, SCHEDULED OR UNSCHEDULED, ACTS OF GOD, UNAUTHORIZED ACCESS, VIRUSES,
DENIAL OF SERVICE OR OTHER ATTACKS, TECHNICAL FAILURE OF THE UTILITIES AND/OR TELECOMMUNICATIONS
INFRASTRUCTURE OR DISRUPTION, AND THEREFORE WE EXPRESSLY DISCLAIM ANY EXPRESS OR IMPLIED WARRANTY
REGARDING THE USE AND/OR AVAILABILITY, ACCESSIBILITY, SECURITY OR PERFORMANCE OF THE UTILITIES CAUSED
BY SUCH FACTORS. WE DO NOT MAKE ANY REPRESENTATIONS OR WARRANTIES AGAINST THE POSSIBILITY OF
DELETION, MISDELIVERY OR FAILURE TO STORE COMMUNICATIONS, PERSONALIZED SETTINGS OR OTHER DATA.
SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES. ACCORDINGLY, SOME OF THE
ABOVE DISCLAIMERS OF WARRANTIES MAY NOT APPLY TO YOU BUT OTHERS REMAIN IN EFFECT.
</v-card-text>
<v-card-text>
You understand that your use of the Utilities is entirely at your own risk. You assume all risks associated with
using the Utilities, and digital assets and decentralized systems generally, including but not limited to, that
digital assets are highly volatile; you may not have ready access to assets; and you may lose some or all of your
tokens or other assets, including with respect to an Interaction or the Vault. You agree that you will have no
recourse against Dexorder for any losses due to your use of the Utilities. For example, these losses may arise
from or relate to: (i) lost funds; (ii) server failure or data loss; (iii) corrupted digital wallet files; (iv)
unauthorized
access; (v) errors, mistakes, or inaccuracies; or (vi) third-party activities.
</v-card-text>
<v-card-title>12. Assumption of Risk.</v-card-title>
<v-card-text>You accept, acknowledge and assume the following risks:</v-card-text>
<v-card-text>
(a) You are solely responsible for determining what, if any, Taxes apply to your transactions
through the Utilities. Neither Dexorder nor any Dexorder affiliates are responsible for determining the Taxes
that apply to such transactions.
</v-card-text>
<v-card-text>
(b) A lack of use or public interest in the creation and development of distributed
ecosystems could negatively impact the development of those ecosystems and related applications, and could
therefore also negatively impact the potential utility or value of certain digital assets.
</v-card-text>
<v-card-text>
(c) By accessing and using the Utilities, you represent that you understand the inherent
risks associated with using cryptographic and blockchain-based systems, and that you have a working knowledge
of the usage and intricacies of tokens such as, bitcoin (BTC), ether (ETH), and other digital tokens such as those
following the Ethereum Token Standard (ERC-20). You further understand that the markets for tokens can be
highly volatile due to factors including (but not limited to) adoption, speculation, technology, security, and
regulation. You acknowledge that the cost and speed of transacting with cryptographic and blockchain-based
systems are variable and may increase at any time. Accordingly, you understand and agree to assume full
responsibility for all of the risks of accessing and using and engaging with the Utilities.
</v-card-text>
<v-card-title>13. Indemnity</v-card-title>
<v-card-text>
You will indemnify, defend (at Dexorders option) and hold Dexorder and its affiliates and their respective
officers,
directors, employees and agents, harmless from and against any claims, disputes, demands, liabilities, damages,
losses,
and costs and expenses, including, without limitation, reasonable legal and accounting fees arising out of or in
any way
connected with: (a) your access to or use of the Utilities, (b) Interactions and the Vault, (c) your violation of
these
Terms, or (d) your negligence, willful misconduct, fraud, or violation of Applicable Laws. You may not settle or
otherwise compromise any claim subject to this Section without Dexorders prior written approval.
</v-card-text>
<v-card-title>14. Limitation of Liability</v-card-title>
<v-card-text>
(a) TO THE MAXIMUM EXTENT PERMITTED BY LAW, NEITHER DEXORDER NOR ITS SERVICE
PROVIDERS INVOLVED IN CREATING, PRODUCING, OR DELIVERING THE UTILITIES WILL BE LIABLE FOR ANY
INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES, OR DAMAGES FOR LOST PROFITS, LOST
REVENUES, LOST SAVINGS, LOST BUSINESS OPPORTUNITY, LOSS OF DATA OR GOODWILL, SERVICE
INTERRUPTION, COMPUTER DAMAGE OR SYSTEM FAILURE OR THE COST OF SUBSTITUTE SERVICES OF ANY KIND
ARISING OUT OF OR IN CONNECTION WITH THESE TERMS OR FROM THE USE OF OR INABILITY TO USE THE
UTILITIES, WHETHER BASED ON WARRANTY, CONTRACT, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY
OR ANY OTHER LEGAL THEORY, AND WHETHER OR NOT DEXORDER OR ITS SERVICE PROVIDERS HAS BEEN
INFORMED OF THE POSSIBILITY OF SUCH DAMAGE, EVEN IF A LIMITED REMEDY SET FORTH HEREIN IS FOUND TO
HAVE FAILED OF ITS ESSENTIAL PURPOSE.
</v-card-text>
<v-card-text>
(b) TO THE MAXIMUM EXTENT PERMITTED BY THE LAW, IN NO EVENT WILL DEXORDERS
TOTAL LIABILITY ARISING OUT OF OR IN CONNECTION WITH THESE TERMS OR FROM THE USE OF OR INABILITY
TO USE THE UTILITIES EXCEED THE TOTAL FEES YOU HAVE PAID OR ARE PAYABLE BY YOU TO DEXORDER FOR USE
OF THE UTILITIES (EXCLUDING GAS FEES), OR ONE HUNDRED DOLLARS ($100) IF YOU HAVE NOT HAD ANY
PAYMENT OBLIGATIONS TO DEXORDER, AS APPLICABLE.
</v-card-text>
<v-card-text>
(c) THE EXCLUSIONS AND LIMITATIONS OF DAMAGES SET FORTH ABOVE ARE
FUNDAMENTAL ELEMENTS OF THE BASIS OF THE BARGAIN BETWEEN DEXORDER AND YOU.
</v-card-text>
<v-card-title>15. Governing Law and Forum Choice</v-card-title>
<v-card-text>
These Terms will be governed by and construed in accordance with the laws of the British Virgin Islands without
regard
to its conflict of laws provisions.
</v-card-text>
<v-card-title>16. Dispute Resolution</v-card-title>
<v-card-text>
(a) <u>Mandatory Arbitration of Disputes</u>. We each agree that any dispute, claim or
controversy arising out of or relating to these Terms or the breach, termination, enforcement, interpretation or
validity thereof or the use of the Utilities (collectively, <b>Disputes</b>) will be resolved <b>solely by
binding, individual
arbitration and not in a class, representative or consolidated action or proceeding.</b> You and Dexorder agree
that the Cayman Islands Arbitration Law governs the interpretation and enforcement of these Terms, and that
you and Dexorder are each waiving the right to a trial by jury or to participate in a class action. This
arbitration
provision shall survive termination of these Terms.
</v-card-text>
<v-card-text>
(b) <u>Exceptions</u>. As limited exceptions to Section 16.(a) above: (i) each party may seek to
resolve a Dispute in small claims court if it qualifies; and (ii) each party retains the right to seek injunctive
or
other equitable relief from a court to prevent (or enjoin) the infringement or misappropriation of our
intellectual
property rights.
</v-card-text>
<v-card-text>
(c) <u>Conducting Arbitration and Arbitration Rules</u>. The arbitration will be conducted by the
Cayman International Mediation &amp; Arbitration Centre (CI-MAC) in accordance with its arbitration rules in force
at the time of the dispute (<b>CI-MAC Rules</b>), except as modified by these Terms. The CI-MAC Rules are
available
at <a href="https://www.caymanarbitration.com/arbitrationrules2023">https://www.caymanarbitration.com/arbitrationrules2023</a>.
A party who wishes to start arbitration must
submit a written request for arbitration to CI-MAC and give notice to the other party as specified in the CI-MAC
Rules. CI-MAC provides instructions on submitting a request for arbitration under Section 3 (Request for
arbitration) of the CI-MAC Rules.
</v-card-text>
<v-card-text>
Any arbitration hearings will take place in the county (or parish) where you live, unless the parties agree to a
different location. The parties agree that the arbitrator shall have exclusive authority to decide all issues
relating
to the interpretation, applicability, enforceability and scope of this arbitration agreement.
</v-card-text>
<v-card-text>
(d) <u>Arbitration Costs</u>. Payment of all filing, administration and arbitrator fees will be
governed by the JAMS Rules, and we wont seek to recover the administration and arbitrator fees we are
responsible for paying, unless the arbitrator finds your Dispute frivolous. If we prevail in arbitration well pay
all
of our attorneys fees and costs and wont seek to recover them from you. If you prevail in arbitration you will
be entitled to an award of attorneys fees and expenses to the extent provided under Applicable Law.
</v-card-text>
<v-card-text>
(e) <u>Injunctive and Declaratory Relief</u>. Except as provided in Section 16.(b) above, the
arbitrator shall determine all issues of liability on the merits of any claim asserted by either party and may
award
declaratory or injunctive relief only in favor of the individual party seeking relief and only to the extent
necessary to provide relief warranted by that partys individual claim. To the extent that you or we prevail on a
claim and seek public injunctive relief (that is, injunctive relief that has the primary purpose and effect of
prohibiting unlawful acts that threaten future injury to the public), the entitlement to and extent of such relief
must be litigated in a civil court of competent jurisdiction and not in arbitration. The parties agree that
litigation
of any issues of public injunctive relief shall be stayed pending the outcome of the merits of any individual
claims in arbitration.
</v-card-text>
<v-card-text>
(f) <u>Class Action Waiver</u>. <b>YOU AND DEXORDER AGREE THAT EACH MAY BRING CLAIMS
AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS
MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING.</b> Further, if the parties Dispute is
resolved through arbitration, the arbitrator may not consolidate another persons claims with your claims, and
may not otherwise preside over any form of a representative or class proceeding. If this specific provision is
found to be unenforceable, then the entirety of this Dispute Resolution section shall be null and void.
</v-card-text>
<v-card-text>
(g) <u>Severability</u>. With the exception of any of the provisions in Section 16.(f) of these Terms
(<b>Class Action Waiver</b>), if an arbitrator or court of competent jurisdiction decides that any part of these
Terms
is invalid or unenforceable, the other parts of these Terms will still apply.
</v-card-text>
<v-card-title>17. General Terms</v-card-title>
<v-card-text>
(a) <u>Reservation of Rights</u>. Dexorder and its licensors exclusively own all right, title and
interest in and to the Dexorder Service, including all associated intellectual property rights. You acknowledge
that the Dexorder Service is protected by copyright, trademark, and other laws of the United States and foreign
countries. You agree not to remove, alter or obscure any copyright, trademark, service mark or other
proprietary rights notices incorporated in or accompanying the Dexorder Service.
</v-card-text>
<v-card-text>
(b) <u>Entire Agreement</u>. These Terms, including any addendum terms, constitute the entire
and exclusive understanding and agreement between Dexorder and you regarding the Dexorder Service, and
these Terms supersede and replace all prior oral or written understandings or agreements between Dexorder
and you regarding the Dexorder Service. If any provision of these Terms is held invalid or unenforceable by an
arbitrator or a court of competent jurisdiction, that provision will be enforced to the maximum extent
permissible and the other provisions of these Terms will remain in full force and effect. Except where provided
by Applicable Law in your jurisdiction, you may not assign or transfer these Terms, by operation of law or
otherwise, without Dexorders prior written consent. Any attempt by you to assign or transfer these Terms
absent our consent or your statutory right, will be null. Dexorder may freely assign or transfer these Terms
without restriction. Subject to the foregoing, these Terms will bind and inure to the benefit of the parties,
their
successors and permitted assigns.
</v-card-text>
<v-card-text>
(c) <u>Notices</u>. Any notices or other communications provided by Dexorder under these Terms
will be given: (i) via email; or (ii) by posting to the Dexorder Service. For notices made by email, the date of
receipt will be deemed the date on which such notice is transmitted.
</v-card-text>
<v-card-text>
(d) <u>Waiver of Rights</u>. Dexorders failure to enforce any right or provision of these Terms will
not be considered a waiver of such right or provision. The waiver of any such right or provision will be effective
only if in writing and signed by a duly authorized representative of Dexorder. Except as expressly set forth in
these Terms, the exercise by either party of any of its remedies under these Terms will be without prejudice to
its other remedies under these Terms or otherwise.
</v-card-text>
<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.com">legal@dexorder.com</a> or <a href="mailto:support@dexorder.com">support@dexorder.com</a>.
</v-card-text>
<slot/>
</v-card>
</template>
<script>
export default {
name: 'tos-card'
}
</script>
<style scoped lang="scss">
.ii {
margin-left: 2em;
}
</style>

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

@@ -36,10 +36,10 @@ const showDialog = ref(false)
const upgradeResult = ref(null)
detectUpgrade().then((version)=>{
if (version===0) {
console.log('Vault is the latest version')
if (version===null) {
console.log('detectUpgrade failed')
}
else {
else if (s.version<version) {
upgradeVersion.value = version
console.log(`Vault upgrade available to version ${version}`)
}

View File

@@ -34,12 +34,10 @@
<v-card-item v-if="!empty">
<v-table>
<tbody>
<suspense>
<native-row v-if="nativeBalance" :chain-id="s.chainId" :addr="s.vault" :amount="nativeBalance"
:on-withdraw="onWithdrawNative" :on-wrap="()=>wrapShow=true"/>
</suspense>
<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>
@@ -62,13 +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 {getToken} from "@/blockchain/token.js";
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()
@@ -91,9 +89,8 @@ const hasVault = computed(()=>s.vault!==null)
const withdrawToken = ref(null)
const withdrawShow = ref(false)
async function onWithdraw(addr) {
const token = await getToken(s.chainId, addr)
console.log('withdraw', addr, token)
async function onWithdraw(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>
@@ -30,16 +30,23 @@ import {tokenFloat} from "@/misc.js";
import {vaultContract} from "@/blockchain/contract.js"
import {pendTransaction} from "@/blockchain/wallet.js";
import {FixedNumber} from "ethers";
import {WithdrawTransaction} from "@/blockchain/transaction.js";
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() {
@@ -50,10 +57,7 @@ function withdraw() {
if( amount === 0n )
return
console.log('pending withdrawal', valueStr, amount, props.token.s)
pendTransaction(async (signer)=>{
const vault = await vaultContract(vaultAddr, signer)
return await vault['withdraw(address,uint256)'](props.token.a, amount)
})
new WithdrawTransaction(s.chainId, vaultAddr, props.token, amount).submit()
floatAmount.value = 0
emit('update:modelValue', false)
}

View File

@@ -26,9 +26,8 @@
<script setup>
import {useStore} from "@/store/store";
import {computed, ref} from "vue";
import {vaultContract} from "@/blockchain/contract.js"
import {pendTransaction} from "@/blockchain/wallet.js";
import {FixedNumber} from "ethers";
import {WithdrawNativeTransaction} from "@/blockchain/transaction.js";
const s = useStore()
const props = defineProps(['modelValue', 'vault', 'balance'])
@@ -44,11 +43,7 @@ function withdraw() {
console.log('pending native withdrawal', valueStr, amount)
if( amount === 0n )
return
pendTransaction(async (signer)=>{
const vault = await vaultContract(vaultAddr, signer)
console.log('invoking withdraw', vaultAddr, amount)
return await vault['withdraw(uint256)'](amount)
})
new WithdrawNativeTransaction(s.chainId, vaultAddr, amount).submit()
floatAmount.value = 0
emit('update:modelValue', false)
}

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"/>
<div :style="faintColorStyle" 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>
<row-bar :color="sideColor">
<div style="width: 100%" class="justify-start align-content-start">
<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,30 +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 {Exchange, newOrder} from "@/blockchain/orderlib.js";
import {lookupSymbol} from "@/charts/datafeed.js";
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))
}
@@ -115,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;
@@ -128,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),
}
}
@@ -157,63 +188,32 @@ 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 lightColor = computed(() => lightenColor(color.value))
const faintColor = computed(() => lightenColor2(color.value))
const colorStyle = computed(() => { return {'color': color.value} })
const bgColorStyle = computed(() => { return {'background-color': color.value} })
const lightColorStyle = computed(() => { return {'background-color': lightColor.value} })
const faintColorStyle = computed(() => { return {'background-color': faintColor.value} })
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} })
// const bgColorStyle = computed(() => { return {'background-color': color.value} })
// 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;
}
}
</style>
@use "src/styles/vars" as *;
.v-btn.green:hover {color: $green !important;}
.v-btn.red:hover {color: $red !important;}
</style>

View File

@@ -7,48 +7,83 @@
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>
<needs-chart>
<template v-for="o in co.orders">
<chart-order :order="o"/>
</template>
<v-dialog v-model="showResetDialog" max-width="300">
<v-card title="Cancel Order?" text="Do you want to cancel this order and start again?">
<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-card-actions>
</v-card>
</v-dialog>
</needs-chart>
<div class="overflow-y-auto">
<needs-chart>
<template v-for="o in co.orders">
<chart-order :order="o"/>
</template>
<v-dialog v-model="showResetDialog" max-width="300">
<v-card title="Cancel Order?" text="Do you want to cancel this order and start again?">
<v-card-actions>
<v-spacer/>
<v-btn @click="()=>showResetDialog=false">Keep Existing</v-btn>
<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>
</needs-chart>
</div>
</toolbar-pane>
</template>
<script setup>
import {orderFuncs, useChartOrderStore} from "@/orderbuild.js";
import {addSymbolChangedCallback, removeSymbolChangedCallback} from "@/charts/chart.js";
import {computed, onBeforeUnmount, ref} from "vue";
import {computed, onBeforeUnmount, ref, toRaw, watchEffect} from "vue";
import {useOrderStore, useStore} from "@/store/store.js";
import {routeFinder} from "@/blockchain/route.js";
import ChartOrder from "@/components/chart/ChartOrder.vue";
import {useTheme} from "vuetify";
import {pendOrder} from "@/blockchain/wallet.js";
import {useWalletStore} from "@/blockchain/wallet.js";
import ToolbarPane from "@/components/chart/ToolbarPane.vue";
import NeedsChart from "@/components/NeedsChart.vue";
import {nav} from "@/misc.js";
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()
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()
}
@@ -62,7 +97,7 @@ const theme = useTheme().current
const orderColor = computed(()=>co.orders.length===0?null : co.orders[0].buy ? theme.value.colors.success:theme.value.colors.error)
const valid = computed(()=>{
if (!s.allowed)
if (!s.approved)
return false
if (co.drawing)
return false
@@ -75,34 +110,138 @@ 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) {
console.log('pend state', order.state)
switch (order.state) {
case PendingOrderState.Sent:
break
case PendingOrderState.Rejected:
removable.push(order)
break
case PendingOrderState.Signing:
break
case PendingOrderState.Submitted:
removable.push(order)
break
default:
console.error('unknown pend state', order.state)
}
}
if (removable.length>0) {
if (ws.pendingOrders.length!==removable.length)
console.error('Not all orders were rejected') // todo multiple orders / order group
ws.pendingOrders = []
}
})
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 (built.length !== 1) {
console.error('Multiple orders not supported')
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 {
await pendOrder(built[0])
co.resetOrders()
nav('Status')
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">
<style lang="scss"> // NOT scoped
body {
overflow-y: hidden;
}

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

@@ -8,7 +8,7 @@ import {computed} from "vue";
const props = defineProps(['color'])
const computedStyle = computed(() => {
return {
'background-color': props.color,
// 'background-color': props.color,
'border-color': props.color,
'width': '.67em',
'border-top-width': '1px',

View File

@@ -1,246 +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 {useTheme} from "vuetify";
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 theme = useTheme().current
const props = defineProps(['order', 'builder'])
const emit = defineEmits(['update:builder'])
const minWidth = computed(()=>co.intervalSecs)
const stdWidth = computed(()=>10 * minWidth.value)
function computeDefaultColor() {
const index = props.order.builders.indexOf(props.builder)
return sideColor(props.order.buy, index)
}
const defaultColor = computeDefaultColor()
const color = computed(()=>computeDefaultColor())
const defaultTranches = 10
builderDefaults(props.builder, {
timeA: s.clock, // todo relative 0
timeB: null,
startTime: s.clock, // todo relative 0
endTime: s.clock + defaultTranches * co.intervalSecs,
interval: co.intervalSecs,
tranches: defaultTranches,
percentage: 100/defaultTranches,
// relative: true, // todo
relative: false,
slippage: DEFAULT_SLIPPAGE,
rungs: 1,
skew: 0,
color: defaultColor,
color: color.value,
valid: true,
})
const times = ref([])
const weights = ref([])
function adjustShapes() {}
const amountSymbol = computed(()=>props.order.amountIsTokenA ? co.selectedSymbol.base.s : co.selectedSymbol.quote.s )
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(w, w*props.order.amount, amountSymbol.value)))
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: ets[i],
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">
@@ -248,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'])
@@ -112,7 +120,6 @@ function computeDefaultColor() {
}
const defaultColor = computeDefaultColor()
const color = computed(()=>props.builder.color ? props.builder.color : defaultColor)
// Fields must be defined in order to be reactive
@@ -122,9 +129,10 @@ 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(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')+' Diagonal'+(weights.value.length>1?'s':''))
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

@@ -1,4 +1,5 @@
<template>
</template>
<script setup>

View File

@@ -1,5 +1,5 @@
<template>
<rung-builder :name="(builder.breakout?'Breakout':'Limit')+(prices.length>1?' Ladder':'')"
<rung-builder :name="(builder.breakout?(order.buy?'Breakout':'Breakdown'):'Limit')+(builder.rungs>1?' Ladder':'')"
:description="description"
:order="order" :builder="builder"
v-model="priceEndpoints" :mode="0" :flip="flipped"
@@ -7,7 +7,7 @@
:get-model-value="getModelValue" :set-model-value="setModelValue"
:set-values="setPrices" :set-weights="setWeights"
:std-width="stdWidth" :build-tranches="buildTranches">
<table>
<table class="rb">
<tbody>
<template v-if="prices.length>1">
<tr>
@@ -15,14 +15,13 @@
<v-text-field type="number" v-model="higherPrice" min="0"
density="compact" hide-details class="mx-1 my-2" variant="outlined"
label="Price"
:color="color" :base-color="color"
style="flex: 6em"
/>
</td>
<td class="weight">{{ allocationTexts[higherIndex] }}</td>
<td class="weight" style="vertical-align: bottom">{{ allocationTexts[higherIndex] }}</td>
</tr>
<tr v-for="i in innerIndexes" class="ml-5">
<td class="pl-5">{{ prices[i] }}</td>
<td class="pl-5">{{ toPrecision(prices[i],6) }}</td>
<td class="weight">{{ allocationTexts[i] }}</td>
</tr>
</template>
@@ -31,14 +30,17 @@
<v-text-field type="number" v-model="lowerPrice" min="0"
density="compact" hide-details class="mx-1 my-2" variant="outlined"
label="Price"
:color="color" :base-color="color"
style="flex: 6em"
/>
</td>
<td class="weight">{{ weights.length ? allocationTexts[lowerIndex] : '' }}</td>
<td class="weight">{{ weights.length > 1 ? allocationTexts[lowerIndex] : '' }}</td>
</tr>
</tbody>
</table>
<one-time-hint name="click-chart" activator="#tv-widget" location="center"
:when="priceA===null" text="Click the chart!"
:on-complete="()=>track('click-chart')"
/>
</rung-builder>
</template>
@@ -50,6 +52,9 @@ import {MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
import RungBuilder from "@/components/chart/RungBuilder.vue";
import {computed, ref} from "vue";
import {allocationText, HLine} from "@/charts/shape.js";
import OneTimeHint from "@/components/OneTimeHint.vue";
import {track} from "@/track.js";
import {toPrecision, toPrecisionOrNull} from "@/misc.js";
const s = useStore()
const os = useOrderStore()
@@ -71,15 +76,17 @@ builderDefaults(props.builder, {
priceA: null,
priceB: null,
rungs: 1,
skew: 0,
balance: 0,
breakout: false,
color: defaultColor,
buy: true,
})
function buildTranches() {
const order = props.order
const builder = props.builder
const tranches = []
const warnings = []
console.log('buildTranches', builder, order, tranches)
const ps = prices.value
@@ -88,17 +95,16 @@ function buildTranches() {
let p = ps[i]
const w = ws[i]
const t = newTranche({
// todo start/end
fraction: w * MAX_FRACTION,
})
const symbol = co.selectedSymbol
console.log('symbol', symbol, p)
// console.log('symbol', symbol, p)
applyLinePoint(t, symbol, order.buy, p, builder.breakout)
tranches.push(t)
}
if (!flipped.value)
tranches.reverse()
return tranches
return {tranches, warnings}
}
@@ -133,10 +139,8 @@ const priceEndpoints = computed({
function update(a, b) {
_priceEndpoints.value = [a, b]
const newBuilder = {...props.builder}
newBuilder.priceA = a
newBuilder.priceB = b
emit('update:builder', newBuilder)
props.builder.priceA = a
props.builder.priceB = b
}
const flipped = computed(()=>{
@@ -146,7 +150,7 @@ const flipped = computed(()=>{
})
const higherPrice = computed({
get() { return flipped.value ? priceA.value : priceB.value },
get() { return toPrecisionOrNull(flipped.value ? priceA.value : priceB.value, 6) },
set(v) {
if (flipped.value)
priceA.value = v
@@ -167,9 +171,7 @@ const innerIndexes = computed(()=>{
})
const lowerPrice = computed({
get() {
return !flipped.value ? priceA.value : priceB.value
},
get() {return toPrecisionOrNull(!flipped.value ? priceA.value : priceB.value, 6)},
set(v) {
if (!flipped.value)
priceA.value = v
@@ -195,13 +197,14 @@ function setPrices(ps) {prices.value = ps}
function setWeights(ws) { weights.value = ws }
const amountSymbol = computed(()=>props.order.amountIsTokenA ? co.selectedSymbol.base.s : co.selectedSymbol.quote.s )
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(w, w*props.order.amount, amountSymbol.value)))
const allocationTexts = computed(()=>weights.value.map((w)=>allocationText(props.order.buy, w, w * props.order.amount, co.selectedSymbol.base.s, amountSymbol.value)))
const color = computed(()=>props.builder.color ? props.builder.color : defaultColor)
const stdWidth = computed(()=>co.meanRange)
const description = computed(()=>{
const buy = props.order.buy
const above = buy === props.builder.breakout
return (buy?'Buy ':'Sell ')+(above?'above':'below')+' the line'
const plural = props.builder.rungs > 1 ? 's' : ''
return (buy?'Buy ':'Sell ')+(above?'above':'below')+' the line'+plural
})
function getModelValue(model) {
@@ -227,5 +230,16 @@ td.weight {
padding-right: 0.5em;
text-align: right;
}
table.rb {
padding: 0;
border-spacing: 0;
tbody {
border: none;
padding: 0;
}
td {
vertical-align: top;
}
}
</style>

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,19 +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" @click="props.builder.breakout=!props.builder.breakout">{{ name }}</v-btn>
<div class="description">{{description}}</div>
<v-btn variant="outlined" style="width: 8em"
@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: 4.5em;"
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/>
@@ -22,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>
@@ -46,13 +92,19 @@ import {cancelDrawing} from "@/charts/chart.js";
import {
devectorize,
vectorAdd,
vectorDiv, vectorEquals,
vectorDiv,
vectorEquals,
vectorIsNull,
vectorIsZero,
vectorize,
vectorMul, vectorNeg,
vectorMul,
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
@@ -65,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
@@ -77,22 +129,35 @@ 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)
})
let lastBuy = null
watchEffect(()=>{
if (props.order.buy!==lastBuy) {
lastBuy = props.order.buy
props.builder.color=computeDefaultColor()
}
})
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)]
}
@@ -115,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)
@@ -133,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)
@@ -184,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
@@ -217,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 {}
})
@@ -251,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)) {
@@ -320,11 +382,12 @@ function makeModel(index) {
allocation: alloc,
maxAllocation: Math.max(...weights.value),
amount: props.order.amount * alloc,
baseSymbol: co.selectedSymbol.base.s,
amountSymbol: amountSymbol.value,
textLocation: above ? 'above' : 'below',
extraText: !props.builder.breakout ? ' ' :
// (above ? '↑ Breakout ↑' : '↓ Breakout ↓')
(above ? '▲ Breakout ▲' : '▼ Breakout ▼')
breakout: props.builder.breakout,
extraText: null,
buy,
}
setModelValue(result, values.value[index])
return result
@@ -404,6 +467,8 @@ function deleteShapes() {
if (!endpoints.value[0])
shapeA.createOrDraw(); // initiate drawing mode
else
adjustShapes()
</script>
@@ -417,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" @click="nav('Order')" :show-tag="true"/>
<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,31 +10,18 @@
<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"/>
<v-btn variant="text" icon="mdi-help-circle-outline" text="Info" @click="showCorp"></v-btn>
<toolbar-button tooltip="About" icon="mdi-information-outline" href="https://dexorder.com/" target="dexorderwww"/>
</div>
</div>
</template>
<script setup>
import {useStore} from "@/store/store.js";
import {useChartOrderStore} from "@/orderbuild.js";
import {useTheme} from "vuetify";
import ToolbarButton from "@/components/chart/ToolbarButton.vue";
import beta from "@/components/Beta.vue";
import Logo from "@/components/Logo.vue";
import {nav} from "@/misc.js";
import {router} from "@/router/router.js";
const props = defineProps(['title', 'icon'])
const s = useStore()
const co = useChartOrderStore()
const theme = useTheme().current
function showCorp() {
window.open('https://dexorder.trade/', 'dexorder')
}
</script>
<style scoped lang="scss">

View File

@@ -2,20 +2,34 @@
<v-tooltip :text="tooltip" location="top">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" :color="isCurrent?'primary':undefined" variant="text"
:icon="icon" @click="()=>nav(route)"/>
:icon="icon" @click="click"/>
</template>
</v-tooltip>
</template>
<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'])
console.log('toolbar button props', props.route)
const router = useRoute();
const props = defineProps(['icon', 'route', 'tooltip', 'href', 'target'])
const isCurrent = computed(() => router.name === props.route)
function click() {
if (!props.href && !props.route)
console.warn('must set href or route in toolbar-button')
else if (props.href && props.route)
console.warn('cannot set both rout and href in toolbar-button')
else if (props.href) {
if (props.target)
window.open(props.href, props.target)
else
window.location.href = props.href
}
else
// noinspection JSIgnoredPromiseFromCall
router.push({name: props.route})
}
</script>
<style scoped lang="scss">

View File

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

82
src/corp/Fees.vue Normal file
View File

@@ -0,0 +1,82 @@
<template>
<v-card title="Dexorder Fee Schedule">
<h2>Fill Fee</h2>
<v-card-text>
Dexorder charges the same fee as the Uniswap pool you trade, capped at a maximum of 0.15%.
</v-card-text>
<v-table>
<thead>
<tr>
<th>Uniswap Pool Fee</th>
<th>Dexorder Fee</th>
<th>Total Fee</th>
</tr>
</thead>
<tbody>
<tr><td>0.01%</td><td>0.01%</td><td>0.02%</td></tr>
<tr><td>0.05%</td><td>0.05%</td><td>0.10%</td></tr>
<tr><td>0.30%</td><td>0.15%</td><td>0.45%</td></tr>
<tr><td>1.00%</td><td>0.15%</td><td>1.15%</td></tr>
</tbody>
</v-table>
<h2>Gas Fees</h2>
<v-card-text>
In order to execute multi-tranche orders at various future times, Dexorder charges an up-front gas fee to cover
the cost of all future smart contract calls required to fill your order. These gas fees depend on how many
tranches are in your order and whether the order triggers a conditional order (stop-loss or take-profit). These
gas fees are collected in native currency (ETH) and are included in your initial order placement transaction.
This means your wallet will display the total gas cost for your entire order when you first place it.
</v-card-text>
<v-card-text>
Once you place your order, you will never be charged additional gas fees. Dexorder's up-front gas charge is
treated like an insurance fund, guaranteeing future execution of your order, even if the price of gas increases
significantly. If your order does not execute all of its tranches or is cancelled, the gas fee is not refunded.
</v-card-text>
<v-card-text>
Dexorder does not seek to profit from gas fees. They are designed to cover our cost of triggering your order
multiple times in the future.
</v-card-text>
<v-card-text>
The formula for computing your gas cost is:
```javascript
// Standard order without any conditional order
totalGasCost = trancheFee * numberOfTranches;
// Order which triggers a conditional order (stop-loss or take-profit)
totalGasCost = (trancheFee + orderFee) * estimatedExecutions + trancheFee * estimatedConditionalExecutions;
estimatedExecutions = numberOfTranches / rateLimit;
```
`trancheFee` and `orderFee` are specified by the `FeeManager` contract and may be adjusted on an hourly basis.
These fee adjustments have important limits and cannot be set to arbitrarily high values. See the
<a href="https://github.com/dexorder-trade/contract/blob/master/src/core/FeeManager.sol">
FeeManager source code
</a>, which describes the security measures.
The `estimatedExecutions` is computed as the number of tranches in the order, divided by any rate limit. For
example, if the rate limit is 20% per hour, then there will be at least 5 executions required to fill the total
amount, so the number of tranches is multiplied by 5 (same as dividing by 20%).
View the [source code for fees](https://github.com/dexorder-trade/contract/blob/master/src/core/OrderLib.sol#L20).
</v-card-text>
<v-card-text>
Tranche
</v-card-text>
<v-card-text>
</v-card-text>
</v-card>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>

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