Compare commits
3 Commits
22f2e648a2
...
38fb66c694
| Author | SHA1 | Date | |
|---|---|---|---|
| 38fb66c694 | |||
| 14b8b50812 | |||
| f35b30e337 |
@@ -1,2 +1,3 @@
|
|||||||
VITE_WS_URL=ws://localhost:3001
|
VITE_WS_URL=ws://localhost:3001
|
||||||
|
VITE_SNAPSHOT_URL=http://localhost:3001/snapshot
|
||||||
REQUIRE_AUTH=NOAUTH
|
REQUIRE_AUTH=NOAUTH
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"flexsearch": "^0.7.43",
|
"flexsearch": "^0.7.43",
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
"pinia": "2.1.6",
|
"pinia": "2.1.6",
|
||||||
"pinia-plugin-persistedstate": "^4.1.3",
|
"pinia-plugin-persistedstate": "^4.1.3",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
|
|||||||
28
public/snapshot.js
Normal file
28
public/snapshot.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
console.log('snapshot.js')
|
||||||
|
|
||||||
|
async function getSnapCode() {
|
||||||
|
const formData = new FormData;
|
||||||
|
formData.append('language', 'en');
|
||||||
|
formData.append('timezone', 'Etc/UTC');
|
||||||
|
formData.append('symbol', 'BTC/USD');
|
||||||
|
formData.append('preparedImage', window.snapshotImage, 'blob');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://tradingview.com/snapshot/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: "same-origin",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to upload snapshot:', response.status, response);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await response.text();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading snapshot:', error);
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapCode().then((code)=>console.log('snapshot code', code)).catch((e)=>console.error('snapshot error', e))
|
||||||
@@ -7,6 +7,7 @@ import {defineStore} from "pinia";
|
|||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import {metadataMap, version} from "@/version.js";
|
import {metadataMap, version} from "@/version.js";
|
||||||
import {CancelAllTransaction, TransactionState, TransactionType} from "@/blockchain/transaction.js";
|
import {CancelAllTransaction, TransactionState, TransactionType} from "@/blockchain/transaction.js";
|
||||||
|
import {track} from "@/track.js";
|
||||||
|
|
||||||
|
|
||||||
export let provider = null
|
export let provider = null
|
||||||
@@ -90,6 +91,7 @@ function changeAccounts(chainId, accounts) {
|
|||||||
const addr = accounts[0]
|
const addr = accounts[0]
|
||||||
if (addr !== store.account) {
|
if (addr !== store.account) {
|
||||||
console.log('account logged in', addr)
|
console.log('account logged in', addr)
|
||||||
|
track('login', {chainId, address: addr})
|
||||||
store.account = addr
|
store.account = addr
|
||||||
store.vaults = []
|
store.vaults = []
|
||||||
// one of these two methods will call flushTransactions()
|
// one of these two methods will call flushTransactions()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useChartOrderStore} from "@/orderbuild.js";
|
import {useChartOrderStore} from "@/orderbuild.js";
|
||||||
import {invokeCallbacks, prototype} from "@/common.js";
|
import {invokeCallbacks, prototype} from "@/common.js";
|
||||||
import {DataFeed, defaultSymbol, feelessTickerKey, getAllSymbols, lookupSymbol} from "@/charts/datafeed.js";
|
import {DataFeed, defaultSymbol, feelessTickerKey, getAllSymbols, lookupSymbol} from "@/charts/datafeed.js";
|
||||||
import {intervalToSeconds, SingletonCoroutine, toHuman, toPrecision} from "@/misc.js";
|
import {intervalToSeconds, secondsToInterval, SingletonCoroutine, toHuman, toPrecision} from "@/misc.js";
|
||||||
import {usePrefStore, useStore} from "@/store/store.js";
|
import {usePrefStore, useStore} from "@/store/store.js";
|
||||||
import {tvCustomThemes} from "../../theme.js";
|
import {tvCustomThemes} from "../../theme.js";
|
||||||
|
|
||||||
@@ -58,12 +58,20 @@ export async function setSymbolTicker(ticker) {
|
|||||||
|
|
||||||
|
|
||||||
function changeInterval(interval) {
|
function changeInterval(interval) {
|
||||||
co.intervalSecs = intervalToSeconds(interval)
|
const secs = intervalToSeconds(interval)
|
||||||
|
co.intervalSecs = secs
|
||||||
prefs.selectedTimeframe = interval
|
prefs.selectedTimeframe = interval
|
||||||
DataFeed.intervalChanged(co.intervalSecs)
|
DataFeed.intervalChanged(secs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function changeIntervalSecs(secs) {
|
||||||
|
const interval = secondsToInterval(secs);
|
||||||
|
co.intervalSecs = secs
|
||||||
|
prefs.selectedTimeframe = interval
|
||||||
|
DataFeed.intervalChanged(secs)
|
||||||
|
}
|
||||||
|
|
||||||
function dataLoaded() {
|
function dataLoaded() {
|
||||||
const range = chartMeanRange()
|
const range = chartMeanRange()
|
||||||
console.log('new mean range', range,)
|
console.log('new mean range', range,)
|
||||||
@@ -164,8 +172,8 @@ export function initWidget(el) {
|
|||||||
container: el,
|
container: el,
|
||||||
datafeed: DataFeed, // use this for ohlc
|
datafeed: DataFeed, // use this for ohlc
|
||||||
locale: "en",
|
locale: "en",
|
||||||
disabled_features: ['main_series_scale_menu',],
|
disabled_features: ['main_series_scale_menu','display_market_status',],
|
||||||
enabled_features: ['saveload_separate_drawings_storage',],
|
enabled_features: ['saveload_separate_drawings_storage','snapshot_trading_drawings','show_exchange_logos','show_symbol_logos',],
|
||||||
// drawings_access: {type: 'white', tools: [],}, // show no tools
|
// drawings_access: {type: 'white', tools: [],}, // show no tools
|
||||||
custom_themes: tvCustomThemes,
|
custom_themes: tvCustomThemes,
|
||||||
theme: useStore().theme,
|
theme: useStore().theme,
|
||||||
@@ -193,6 +201,16 @@ export function initWidget(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function onChartReady(f) {
|
||||||
|
if (co.chartReady)
|
||||||
|
f(widget, chart)
|
||||||
|
else
|
||||||
|
chartInitCbs.push(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
let chartInitCbs = []
|
||||||
|
|
||||||
|
|
||||||
function initChart() {
|
function initChart() {
|
||||||
console.log('init chart')
|
console.log('init chart')
|
||||||
chart = widget.activeChart()
|
chart = widget.activeChart()
|
||||||
@@ -217,6 +235,11 @@ function initChart() {
|
|||||||
}
|
}
|
||||||
changeInterval(widget.symbolInterval().interval)
|
changeInterval(widget.symbolInterval().interval)
|
||||||
co.chartReady = true
|
co.chartReady = true
|
||||||
|
setTimeout(()=>{
|
||||||
|
for (const cb of chartInitCbs)
|
||||||
|
cb(widget, chart)
|
||||||
|
chartInitCbs = []
|
||||||
|
}, 1)
|
||||||
console.log('chart ready')
|
console.log('chart ready')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {ohlcStart} from "@/charts/chart-misc.js";
|
|||||||
import {timestamp, USD_FIAT} from "@/common.js";
|
import {timestamp, USD_FIAT} from "@/common.js";
|
||||||
import {erc20Contract} from "@/blockchain/contract.js";
|
import {erc20Contract} from "@/blockchain/contract.js";
|
||||||
import {provider} from "@/blockchain/wallet.js";
|
import {provider} from "@/blockchain/wallet.js";
|
||||||
|
import {track} from "@/track.js";
|
||||||
|
|
||||||
const DEBUG_LOGGING = false
|
const DEBUG_LOGGING = false
|
||||||
const log = DEBUG_LOGGING ? console.log : ()=>{}
|
const log = DEBUG_LOGGING ? console.log : ()=>{}
|
||||||
@@ -63,7 +64,7 @@ const configurationData = {
|
|||||||
value: 'UNIv3',
|
value: 'UNIv3',
|
||||||
name: 'Uniswap v3',
|
name: 'Uniswap v3',
|
||||||
desc: '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
|
// The `symbols_types` arguments are used for the `searchSymbols` method if a user selects this symbol type
|
||||||
@@ -110,8 +111,10 @@ export function feelessTickerKey(ticker) {
|
|||||||
|
|
||||||
function addSymbol(chainId, p, base, quote, inverted) {
|
function addSymbol(chainId, p, base, quote, inverted) {
|
||||||
const symbol = base.s + '/' + quote.s
|
const symbol = base.s + '/' + quote.s
|
||||||
const fee = `${(p.f/10000).toFixed(2)}%`
|
// const fee = `${(p.f/10000).toFixed(2)}%`
|
||||||
const exchange = ['Uniswap v2', 'Uniswap v3'][p.e] + ' ' + fee
|
// 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 full_name = exchange + ':' + symbol // + '%' + formatFee(fee)
|
||||||
const ticker = tickerKey(chainId, p.e, base.a, quote.a, p.f)
|
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
|
// add the search index only if this is the natural, noninverted base/quote pair
|
||||||
@@ -122,7 +125,7 @@ function addSymbol(chainId, p, base, quote, inverted) {
|
|||||||
const symbolInfo = {
|
const symbolInfo = {
|
||||||
key: ticker, ticker,
|
key: ticker, ticker,
|
||||||
chainId, address: p.a, exchangeId: p.e,
|
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
|
_symbols[ticker] = symbolInfo
|
||||||
const feelessKey = feelessTickerKey(ticker)
|
const feelessKey = feelessTickerKey(ticker)
|
||||||
@@ -356,6 +359,8 @@ export const DataFeed = {
|
|||||||
result.push(_symbols[ticker])
|
result.push(_symbols[ticker])
|
||||||
seen[ticker] = true
|
seen[ticker] = true
|
||||||
}
|
}
|
||||||
|
if (userInput.length>=3)
|
||||||
|
track('search', {search_term: userInput})
|
||||||
onResultReadyCallback(result);
|
onResultReadyCallback(result);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ function reload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function connect() {
|
async function connect() {
|
||||||
track('connect_wallet')
|
|
||||||
disabled.value = true
|
disabled.value = true
|
||||||
try {
|
try {
|
||||||
await addNetworkAndConnectWallet(s.chainId);
|
await addNetworkAndConnectWallet(s.chainId);
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ const props = defineProps({
|
|||||||
name: {type: String, required: true},
|
name: {type: String, required: true},
|
||||||
when: {type: Boolean, default: true}, // optional conditional for when to show
|
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.
|
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 forceClose = ref(false)
|
||||||
|
const shown = ref(false)
|
||||||
|
|
||||||
const show = computed({
|
const show = computed({
|
||||||
get() {
|
get() {
|
||||||
@@ -23,11 +25,13 @@ const show = computed({
|
|||||||
const afterOk = props.after === null || prefs.hints[props.after];
|
const afterOk = props.after === null || prefs.hints[props.after];
|
||||||
const result = !forceClose.value && !shownBefore && whenOk && afterOk
|
const result = !forceClose.value && !shownBefore && whenOk && afterOk
|
||||||
// console.log(`show ${props.name}? ${result} <=`, !forceClose.value, whenOk, afterOk, prefs.hints)
|
// console.log(`show ${props.name}? ${result} <=`, !forceClose.value, whenOk, afterOk, prefs.hints)
|
||||||
if (result)
|
if (result) {
|
||||||
|
shown.value = true
|
||||||
prefs.hints[props.name] = true
|
prefs.hints[props.name] = true
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
set(v) { if(!v) forceClose.value=true; }
|
set(v) { if(!v) { forceClose.value=true; if (shown.value && props.onComplete) props.onComplete(); } }
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<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-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-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 prepend-icon="mdi-cancel">One-click Cancel All</v-list-item>-->
|
||||||
|
<!--
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-avatar image="/arbitrum-logo.svg" size="1.5em" class="mr-4"/>
|
<v-avatar image="/arbitrum-logo.svg" size="1.5em" class="mr-4"/>
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
<b>Arbitrum One</b> support <small>fast and cheap</small>
|
<b>Arbitrum One</b> support <small>fast and cheap</small>
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
-->
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-avatar image="/uniswap-logo.svg" size="1.5em" class="mr-4" style="background-color: white"/>
|
<v-avatar image="/uniswap-logo.svg" size="1.5em" class="mr-4" style="background-color: white"/>
|
||||||
@@ -51,7 +53,7 @@ import Logo from "@/components/Logo.vue";
|
|||||||
const modelValue = defineModel()
|
const modelValue = defineModel()
|
||||||
|
|
||||||
function tryIt() {
|
function tryIt() {
|
||||||
track('try-it')
|
track('tutorial_begin')
|
||||||
modelValue.value = false
|
modelValue.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
<!-- mdi-ray-start-end mdi-vector-polyline -->
|
<!-- mdi-ray-start-end mdi-vector-polyline -->
|
||||||
|
|
||||||
<!-- after="newbie"-->
|
<!-- after="newbie"-->
|
||||||
<span>{{builders.length}}</span>
|
|
||||||
<one-time-hint name="choose-builder" :activator="hintData.activator"
|
<one-time-hint name="choose-builder" :activator="hintData.activator"
|
||||||
:text="hintData.text" location="top"
|
:text="hintData.text" location="top"
|
||||||
:when="!builtAny"/>
|
:when="!builtAny"/>
|
||||||
@@ -78,6 +77,7 @@ const co = useChartOrderStore()
|
|||||||
|
|
||||||
const marketBuilder = newBuilder('MarketBuilder')
|
const marketBuilder = newBuilder('MarketBuilder')
|
||||||
|
|
||||||
|
console.log('chart order', props.order)
|
||||||
const builders = computed(()=>props.order.builders.length > 0 ? props.order.builders : [marketBuilder])
|
const builders = computed(()=>props.order.builders.length > 0 ? props.order.builders : [marketBuilder])
|
||||||
const tokenIn = computed(()=>props.order.buy ? co.quoteToken : co.baseToken)
|
const tokenIn = computed(()=>props.order.buy ? co.quoteToken : co.baseToken)
|
||||||
const tokenOut = computed(()=>props.order.buy ? co.baseToken : co.quoteToken)
|
const tokenOut = computed(()=>props.order.buy ? co.baseToken : co.quoteToken)
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn variant="text" prepend-icon="mdi-delete" v-if="co.orders.length>0"
|
<v-btn variant="text" prepend-icon="mdi-delete" v-if="co.orders.length>0"
|
||||||
:disabled="!orderChanged" @click="resetOrder">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'}}</v-btn>
|
||||||
|
<v-tooltip activator="#share-btn" text="Copied share link!" v-model="showSharedTooltip"
|
||||||
|
:open-on-hover="false" :open-on-click="false"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div class="overflow-y-auto">
|
<div class="overflow-y-auto">
|
||||||
<needs-chart>
|
<needs-chart>
|
||||||
@@ -66,6 +72,7 @@ import NeedsChart from "@/components/NeedsChart.vue";
|
|||||||
import {PlaceOrderTransaction} from "@/blockchain/transaction.js";
|
import {PlaceOrderTransaction} from "@/blockchain/transaction.js";
|
||||||
import {errorSuggestsMissingVault} from "@/misc.js";
|
import {errorSuggestsMissingVault} from "@/misc.js";
|
||||||
import {track} from "@/track.js";
|
import {track} from "@/track.js";
|
||||||
|
import {getShareUrl} from "@/share.js";
|
||||||
|
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
const co = useChartOrderStore()
|
const co = useChartOrderStore()
|
||||||
@@ -149,7 +156,7 @@ watchEffect(()=>{
|
|||||||
let built = []
|
let built = []
|
||||||
|
|
||||||
async function placeOrder() {
|
async function placeOrder() {
|
||||||
track('place_order')
|
track('place-order')
|
||||||
const chartOrders = co.orders;
|
const chartOrders = co.orders;
|
||||||
const allWarns = []
|
const allWarns = []
|
||||||
built = []
|
built = []
|
||||||
@@ -196,6 +203,34 @@ async function doPlaceOrder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sharing = ref(false)
|
||||||
|
const showSharedTooltip = ref(false)
|
||||||
|
const showShareDialog = ref(false)
|
||||||
|
const shareUrl = ref(null)
|
||||||
|
|
||||||
|
function shareOrder() {
|
||||||
|
sharing.value = true
|
||||||
|
getShareUrl().then(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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"> // NOT scoped
|
<style lang="scss"> // NOT scoped
|
||||||
|
|||||||
@@ -177,10 +177,8 @@ function update(a, b, updateA, updateB) {
|
|||||||
a = maxA
|
a = maxA
|
||||||
}
|
}
|
||||||
_timeEndpoints.value = [a, b]
|
_timeEndpoints.value = [a, b]
|
||||||
const newBuilder = {...props.builder}
|
props.builder.timeA = a
|
||||||
newBuilder.timeA = a
|
props.builder.timeB = b
|
||||||
newBuilder.timeB = b
|
|
||||||
emit('update:builder', newBuilder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const flipped = computed(()=>{
|
const flipped = computed(()=>{
|
||||||
|
|||||||
@@ -89,13 +89,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<one-time-hint name="click-chart" activator="#tv-widget" location="center" :when="builder.lineA===null && !co.drew" text="Click the chart!"/>
|
<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>
|
</rung-builder>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {applyLinePoints, builderDefaults, useChartOrderStore} from "@/orderbuild.js";
|
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 {MAX_FRACTION, newTranche} from "@/blockchain/orderlib.js";
|
||||||
import RungBuilder from "@/components/chart/RungBuilder.vue";
|
import RungBuilder from "@/components/chart/RungBuilder.vue";
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
@@ -104,6 +107,7 @@ import {vectorEquals, vectorInterpolate} from "@/vector.js";
|
|||||||
import AbsoluteTimeEntry from "@/components/AbsoluteTimeEntry.vue";
|
import AbsoluteTimeEntry from "@/components/AbsoluteTimeEntry.vue";
|
||||||
import {useStore} from "@/store/store.js";
|
import {useStore} from "@/store/store.js";
|
||||||
import OneTimeHint from "@/components/OneTimeHint.vue";
|
import OneTimeHint from "@/components/OneTimeHint.vue";
|
||||||
|
import {track} from "@/track.js";
|
||||||
|
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
const co = useChartOrderStore()
|
const co = useChartOrderStore()
|
||||||
@@ -206,7 +210,7 @@ const time1A = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const price1A = 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) {
|
set(v) {
|
||||||
const flatline0 = _endpoints.value[0];
|
const flatline0 = _endpoints.value[0];
|
||||||
update(
|
update(
|
||||||
@@ -228,7 +232,7 @@ const time1B = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const price1B = 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) {
|
set(v) {
|
||||||
const flatline0 = _endpoints.value[0];
|
const flatline0 = _endpoints.value[0];
|
||||||
update(
|
update(
|
||||||
@@ -250,7 +254,7 @@ const time2A = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const price2A = 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) {
|
set(v) {
|
||||||
const flatline = _endpoints.value[1];
|
const flatline = _endpoints.value[1];
|
||||||
update(
|
update(
|
||||||
@@ -272,7 +276,7 @@ const time2B = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const price2B = 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) {
|
set(v) {
|
||||||
const flatline = _endpoints.value[1];
|
const flatline = _endpoints.value[1];
|
||||||
update(
|
update(
|
||||||
@@ -285,10 +289,8 @@ const price2B = computed({
|
|||||||
function update(a, b) { // a and b are lines of two points
|
function update(a, b) { // a and b are lines of two points
|
||||||
if (!vectorEquals(props.builder.lineA, a) || !vectorEquals(props.builder.lineB, b)) {
|
if (!vectorEquals(props.builder.lineA, a) || !vectorEquals(props.builder.lineB, b)) {
|
||||||
_endpoints.value = [flattenLine(a), flattenLine(b)]
|
_endpoints.value = [flattenLine(a), flattenLine(b)]
|
||||||
const newBuilder = {...props.builder}
|
props.builder.lineA = a
|
||||||
newBuilder.lineA = a
|
props.builder.lineB = b
|
||||||
newBuilder.lineB = b
|
|
||||||
emit('update:builder', newBuilder)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<td class="weight" style="vertical-align: bottom">{{ allocationTexts[higherIndex] }}</td>
|
<td class="weight" style="vertical-align: bottom">{{ allocationTexts[higherIndex] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="i in innerIndexes" class="ml-5">
|
<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>
|
<td class="weight">{{ allocationTexts[i] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -37,7 +37,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<one-time-hint name="click-chart" activator="#tv-widget" location="center" :when="priceA===null" text="Click the chart!"/>
|
<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>
|
</rung-builder>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -50,6 +53,8 @@ import RungBuilder from "@/components/chart/RungBuilder.vue";
|
|||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import {allocationText, HLine} from "@/charts/shape.js";
|
import {allocationText, HLine} from "@/charts/shape.js";
|
||||||
import OneTimeHint from "@/components/OneTimeHint.vue";
|
import OneTimeHint from "@/components/OneTimeHint.vue";
|
||||||
|
import {track} from "@/track.js";
|
||||||
|
import {toPrecision, toPrecisionOrNull} from "@/misc.js";
|
||||||
|
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
const os = useOrderStore()
|
const os = useOrderStore()
|
||||||
@@ -134,10 +139,8 @@ const priceEndpoints = computed({
|
|||||||
|
|
||||||
function update(a, b) {
|
function update(a, b) {
|
||||||
_priceEndpoints.value = [a, b]
|
_priceEndpoints.value = [a, b]
|
||||||
const newBuilder = {...props.builder}
|
props.builder.priceA = a
|
||||||
newBuilder.priceA = a
|
props.builder.priceB = b
|
||||||
newBuilder.priceB = b
|
|
||||||
emit('update:builder', newBuilder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const flipped = computed(()=>{
|
const flipped = computed(()=>{
|
||||||
@@ -147,7 +150,7 @@ const flipped = computed(()=>{
|
|||||||
})
|
})
|
||||||
|
|
||||||
const higherPrice = computed({
|
const higherPrice = computed({
|
||||||
get() { return flipped.value ? priceA.value : priceB.value },
|
get() { return toPrecisionOrNull(flipped.value ? priceA.value : priceB.value, 6) },
|
||||||
set(v) {
|
set(v) {
|
||||||
if (flipped.value)
|
if (flipped.value)
|
||||||
priceA.value = v
|
priceA.value = v
|
||||||
@@ -168,9 +171,7 @@ const innerIndexes = computed(()=>{
|
|||||||
})
|
})
|
||||||
|
|
||||||
const lowerPrice = computed({
|
const lowerPrice = computed({
|
||||||
get() {
|
get() {return toPrecisionOrNull(!flipped.value ? priceA.value : priceB.value, 6)},
|
||||||
return !flipped.value ? priceA.value : priceB.value
|
|
||||||
},
|
|
||||||
set(v) {
|
set(v) {
|
||||||
if (!flipped.value)
|
if (!flipped.value)
|
||||||
priceA.value = v
|
priceA.value = v
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
/>
|
/>
|
||||||
<one-time-hint name="rungs" activator="#rungs" after="choose-builder"
|
<one-time-hint name="rungs" activator="#rungs" after="choose-builder"
|
||||||
text="↓ Try increasing rungs!" location="top"
|
text="↓ Try increasing rungs!" location="top"
|
||||||
:when="rungs===1&&endpoints[0]!==null"/>
|
:when="rungs===1&&endpoints[0]!==null"
|
||||||
|
:on-complete="()=>track('rungs')"
|
||||||
|
/>
|
||||||
<v-tooltip v-if="builder.breakout!==undefined"
|
<v-tooltip v-if="builder.breakout!==undefined"
|
||||||
:text="order.buy?'Breakout orders buy above the breakout line':'Breakdown orders sell below the breakdown line'">
|
:text="order.buy?'Breakout orders buy above the breakout line':'Breakdown orders sell below the breakdown line'">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
@@ -54,7 +56,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<one-time-hint name="balance-slider" activator="#balance-slider" after="rungs"
|
<one-time-hint name="balance-slider" activator="#balance-slider" after="rungs"
|
||||||
text="↓ Slide the amount balance ↓" location="top"
|
text="↓ Slide the amount balance ↓" location="top"
|
||||||
:when="balance100===0"/>
|
:when="balance100===0"
|
||||||
|
:on-complete="()=>track('balance-slider')"
|
||||||
|
/>
|
||||||
<v-text-field type="number" v-model="balance100" min="-100" max="100"
|
<v-text-field type="number" v-model="balance100" min="-100" max="100"
|
||||||
density="compact" hide-details variant="outlined" label="Balance" step="5"
|
density="compact" hide-details variant="outlined" label="Balance" step="5"
|
||||||
class="balance">
|
class="balance">
|
||||||
@@ -88,6 +92,7 @@ import {
|
|||||||
} from "@/vector.js";
|
} from "@/vector.js";
|
||||||
import {logicalXOR} from "@/common.js";
|
import {logicalXOR} from "@/common.js";
|
||||||
import OneTimeHint from "@/components/OneTimeHint.vue";
|
import OneTimeHint from "@/components/OneTimeHint.vue";
|
||||||
|
import {track} from "@/track.js";
|
||||||
|
|
||||||
const co = useChartOrderStore()
|
const co = useChartOrderStore()
|
||||||
const endpoints = defineModel('modelValue') // 2-item list of points/values
|
const endpoints = defineModel('modelValue') // 2-item list of points/values
|
||||||
@@ -121,7 +126,7 @@ const balance100 = computed( {
|
|||||||
watchEffect(()=>{
|
watchEffect(()=>{
|
||||||
const rungs = props.builder.rungs
|
const rungs = props.builder.rungs
|
||||||
// const prev = props.builder.valid
|
// 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)
|
// console.log('valid?', prev, props.builder.valid, rungs, valueA.value, valueB.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -452,6 +457,8 @@ function deleteShapes() {
|
|||||||
|
|
||||||
if (!endpoints.value[0])
|
if (!endpoints.value[0])
|
||||||
shapeA.createOrDraw(); // initiate drawing mode
|
shapeA.createOrDraw(); // initiate drawing mode
|
||||||
|
else
|
||||||
|
adjustShapes()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
14
src/components/chart/Shared.vue
Normal file
14
src/components/chart/Shared.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {loadShareUrl} from "@/share.js";
|
||||||
|
import router from "@/router/index.js";
|
||||||
|
|
||||||
|
loadShareUrl()
|
||||||
|
router.replace('/order')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
21
src/misc.js
21
src/misc.js
@@ -230,6 +230,21 @@ export function intervalToSeconds(interval) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function secondsToInterval(seconds) {
|
||||||
|
const units = [
|
||||||
|
[30 * 24 * 60 * 60, 'M'],
|
||||||
|
[7 * 24 * 60 * 60, 'W'],
|
||||||
|
[24 * 60 * 60, 'D'],
|
||||||
|
[60, ''],
|
||||||
|
[1, 'S'],
|
||||||
|
]
|
||||||
|
for( const [unit, suffix] of units)
|
||||||
|
if (seconds % unit === 0)
|
||||||
|
return `${seconds / unit}${suffix}`
|
||||||
|
throw Error(`invalid secondsToInterval ${seconds}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function interpolate(a, b, zeroToOne) {
|
export function interpolate(a, b, zeroToOne) {
|
||||||
const d = (b-a)
|
const d = (b-a)
|
||||||
return a + d * zeroToOne
|
return a + d * zeroToOne
|
||||||
@@ -260,6 +275,12 @@ export function toPrecision(value, significantDigits = 3) {
|
|||||||
return value.toFixed(decimalsNeeded); // Use toFixed to completely avoid scientific notation
|
return value.toFixed(decimalsNeeded); // Use toFixed to completely avoid scientific notation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toPrecisionOrNull(value, significantDigits = 3) {
|
||||||
|
if (value===null) return null
|
||||||
|
if (value===undefined) return undefined
|
||||||
|
return toPrecision(value, significantDigits)
|
||||||
|
}
|
||||||
|
|
||||||
export function toHuman(value, significantDigits = 2) {
|
export function toHuman(value, significantDigits = 2) {
|
||||||
if (!isFinite(value)) return value.toString(); // Handle Infinity and NaN
|
if (!isFinite(value)) return value.toString(); // Handle Infinity and NaN
|
||||||
let suffix = ''
|
let suffix = ''
|
||||||
|
|||||||
@@ -199,8 +199,9 @@ export function timesliceTranches() {
|
|||||||
|
|
||||||
export function builderDefaults(builder, defaults) {
|
export function builderDefaults(builder, defaults) {
|
||||||
for (const k in defaults)
|
for (const k in defaults)
|
||||||
if (builder[k] === undefined)
|
if (!Object.prototype.hasOwnProperty.call(builder, k)) {
|
||||||
builder[k] = defaults[k] instanceof Function ? defaults[k]() : defaults[k]
|
builder[k] = defaults[k] instanceof Function ? defaults[k]() : defaults[k]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function linearWeights(num, skew) {
|
export function linearWeights(num, skew) {
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ const routes = [
|
|||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import('@/components/chart/ChartPlaceOrder.vue'),
|
component: () => import('@/components/chart/ChartPlaceOrder.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Shared',
|
||||||
|
path: '/shared',
|
||||||
|
component: ()=> import('@/components/chart/Shared.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Order',
|
name: 'Order',
|
||||||
path: '/order',
|
path: '/order',
|
||||||
|
|||||||
74
src/share.js
Normal file
74
src/share.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string';
|
||||||
|
import {useChartOrderStore} from "@/orderbuild.js";
|
||||||
|
import {changeIntervalSecs, onChartReady, setSymbol, widget} from "@/charts/chart.js";
|
||||||
|
import {usePrefStore, useStore} from "@/store/store.js";
|
||||||
|
import {lookupSymbol} from "@/charts/datafeed.js";
|
||||||
|
|
||||||
|
export async function getShareUrl() {
|
||||||
|
const co = useChartOrderStore();
|
||||||
|
const s = useStore()
|
||||||
|
const sym = co.selectedSymbol
|
||||||
|
console.log('symbol', sym)
|
||||||
|
const data = {
|
||||||
|
version: 1,
|
||||||
|
chainId: s.chainId,
|
||||||
|
orders: co.orders,
|
||||||
|
symbol: {
|
||||||
|
base: {a: sym.base.a, s: sym.base.s},
|
||||||
|
quote: {a: sym.quote.a, s: sym.quote.s},
|
||||||
|
route: {
|
||||||
|
fee: sym.fee,
|
||||||
|
exchange: sym.exchangeId,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
period: co.intervalSecs,
|
||||||
|
}
|
||||||
|
const json = JSON.stringify(data)
|
||||||
|
console.log('sharing data', json, data)
|
||||||
|
const compressed = compressToEncodedURIComponent(json);
|
||||||
|
const baseUrl = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
|
||||||
|
const imageFile = await takeSnapshot()
|
||||||
|
return `${baseUrl}/shared?i=${imageFile}&d=${compressed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadShareUrl() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const dataStr = urlParams.get('d');
|
||||||
|
if (!dataStr) return
|
||||||
|
const json = decompressFromEncodedURIComponent(dataStr);
|
||||||
|
const data = JSON.parse(json);
|
||||||
|
console.log('loaded shared orders data', data)
|
||||||
|
const co = useChartOrderStore();
|
||||||
|
const s = useStore()
|
||||||
|
const ticker = `${data.chainId}|${data.symbol.route.exchange}|${data.symbol.base.a}|${data.symbol.quote.a}|${data.symbol.route.fee}`;
|
||||||
|
const symbol = lookupSymbol(ticker)
|
||||||
|
if (symbol===null) {
|
||||||
|
console.error('could not find symbol for ticker', ticker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.chainId = data.chainId
|
||||||
|
const prefs = usePrefStore()
|
||||||
|
prefs.selectedSymbol = ticker
|
||||||
|
for (const order of data.orders) {
|
||||||
|
order.amount = 0
|
||||||
|
order.valid = false
|
||||||
|
}
|
||||||
|
co.orders = data.orders
|
||||||
|
changeIntervalSecs(data.period)
|
||||||
|
onChartReady(()=>{
|
||||||
|
setSymbol(symbol)
|
||||||
|
.catch((e)=>console.error('could not set symbol', e))
|
||||||
|
})
|
||||||
|
console.log('loaded orders', s.chainId, co.orders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function takeSnapshot() {
|
||||||
|
const screenshotCanvas = await widget.takeClientScreenshot();
|
||||||
|
const image = await new Promise((resolve) => screenshotCanvas.toBlob(resolve));
|
||||||
|
const response = await fetch(import.meta.env.VITE_SNAPSHOT_URL, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: image,
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
return response.text()
|
||||||
|
}
|
||||||
@@ -6,7 +6,12 @@ import { DataFeed } from "./charts/datafeed";
|
|||||||
import {notifyFillEvent} from "@/notify.js";
|
import {notifyFillEvent} from "@/notify.js";
|
||||||
import {refreshOHLCSubs} from "@/blockchain/ohlcs.js";
|
import {refreshOHLCSubs} from "@/blockchain/ohlcs.js";
|
||||||
|
|
||||||
export const socket = io(import.meta.env.VITE_WS_URL || undefined, {transports: ["websocket"]})
|
const socketOptions = {
|
||||||
|
transports: ["websocket"],
|
||||||
|
pingInterval: 25000, // PING every 25 seconds
|
||||||
|
pingTimeout: 60000 // Timeout if no PONG in 60 seconds
|
||||||
|
}
|
||||||
|
export const socket = io(import.meta.env.VITE_WS_URL || undefined, socketOptions)
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
console.log(new Date(), 'ws connected')
|
console.log(new Date(), 'ws connected')
|
||||||
|
|||||||
17
src/track.js
17
src/track.js
@@ -1,12 +1,15 @@
|
|||||||
export let tracking_enabled = true
|
export let tracking_enabled = window.gtag !== undefined
|
||||||
|
|
||||||
export function track(event, info) {
|
if(tracking_enabled)
|
||||||
|
console.log('gtag', tracking_enabled)
|
||||||
|
else
|
||||||
|
console.log('tracking disabled')
|
||||||
|
|
||||||
|
export function track(...args) {
|
||||||
if (tracking_enabled) {
|
if (tracking_enabled) {
|
||||||
if (window.gtag !== undefined)
|
try {
|
||||||
window.gtag('event', event, info)
|
window.gtag('event', ...args)
|
||||||
else {
|
} catch (e) {
|
||||||
console.log('gtag not available')
|
|
||||||
tracking_enabled = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1489,6 +1489,11 @@ luxon@^3.4.4:
|
|||||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20"
|
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20"
|
||||||
integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==
|
integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==
|
||||||
|
|
||||||
|
lz-string@^1.5.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||||
|
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||||
|
|
||||||
magic-string@^0.30.11, magic-string@^0.30.17:
|
magic-string@^0.30.11, magic-string@^0.30.17:
|
||||||
version "0.30.17"
|
version "0.30.17"
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
||||||
|
|||||||
Reference in New Issue
Block a user