diff --git a/best_swap_route.go b/best_swap_route.go new file mode 100644 index 0000000..4259232 --- /dev/null +++ b/best_swap_route.go @@ -0,0 +1,4 @@ +package router + +type BestSwapRoute struct { +} diff --git a/core/alpha_router.go b/core/alpha_router.go index f96a4a5..53d33f9 100644 --- a/core/alpha_router.go +++ b/core/alpha_router.go @@ -4,56 +4,97 @@ import "router/core/currency" type AlphaRouter struct { //chainId ChainId - //portionProvider IPortionProvider + portionProvider IPortionProvider } func NewAlphaRouter(params AlphaRouterParams) *AlphaRouter { return &AlphaRouter{} } -func (a AlphaRouter) route( - baseCurrency currency.Currency, // currencyIn - quoteCurrency currency.Currency, // currencyOut으로 바꿔도 될 것 같다. - amount float64, - // amount fractions.CurrencyAmount, - // tradeType TradeType, - // swapConfig SwapOptions, +// Todo: goroutine +func (a *AlphaRouter) route( + baseCurrency currency.Currency, + quoteCurrency currency.Currency, + amount float64, // todo: float64 -> fraction + tradeType TradeType, + swapConfig SwapOptions, + routerConfig AlphaRouterConfig, ) SwapRoute { - //originalAmount := amount - - // currencyIn, currencyOut은 Currency 타입이고 - // Currency 타입은 NativeCurrency(GNOT)이거나 Token 타입이다. - // 아래에서 Token 타입이길 원하는 듯하다. - //tokenIn := currencyIn.Wrapped() - //tokenOut := currencyOut.Wrapped() - - //core 패키지를 TradeType 패키지로 변경하면 가독성이 더 좋아질 듯 하다. - //if tradeType == EXACT_OUTPUT { - // // TODO: GetPortionAmount에서 반환 값인 CurrencyAmount을 반환하지 못할 경우가 있을 수도 있다.(높은 확률로) - // portionAmount := a.portionProvider.GetPortionAmount( - // amount, - // tradeType, - // swapConfig, - // ) - // - //result := portionAmount.GreaterThan(0) - //if result { - // amount = amount.add(portionAmount) - //} + //originalAmount := amount // for save + + //currencyIn, currencyOut := a.determineCurrencyInOutFromTradeType(tradeType, baseCurrency, quoteCurrency) + + // token은 currency의 wrapped된 버전이다. + //tokenIn := currencyIn.GetToken() + //tokenOut := currencyOut.GetToken() + + // 왠만하면 함수로 뺄 것 + // 내용 이해 필요 + if tradeType == EXACT_OUTPUT { + portionAmount, portionErr := a.portionProvider.GetPortionAmount(amount, tradeType, swapConfig) + + if portionErr == nil && portionAmount > 0 { + // In case of exact out swap, before we route, we need to make sure that the + // token out amount accounts for flat portion, and token in amount after the best swap route contains the token in equivalent of portion. + // In other words, in case a pool's LP fee bps is lower than the portion bps (0.01%/0.05% for v3), a pool can go insolvency. + // This is because instead of the swapper being responsible for the portion, + // the pool instead gets responsible for the portion. + // The addition below avoids that situation. + amount += portionAmount + } + } + + // routing config merge다루는 부분 패스 + //routerConfig = setRouterConfig(routingConfig, chainId) + + // tokenIn 또는 tokenOut과 동일한 값... + //quoteToken := quoteCurrency.GetToken() + + // main logic? + //routes := a.getSwapRouteFromChain(tokenIn, tokenOut, amount, tradeType, routingConfig) + + //if routes == nil { + // // todo: error 처리 해 줄 것 //} + //trade := a.buildTrade(currencyIn, currencyOut, tradeType, routes) + + swapRoute := a.buildSwapRoute() + return swapRoute +} + +func (a *AlphaRouter) determineCurrencyInOutFromTradeType( + tradeType TradeType, + baseCurrency currency.Currency, + quoteCurrency currency.Currency, +) (currency.Currency, currency.Currency) { + if tradeType == EXACT_INPUT { + return baseCurrency, quoteCurrency + } + return quoteCurrency, baseCurrency +} + +// todo: goroutine +func (a *AlphaRouter) getSwapRouteFromChain(tokenIn, tokenOut currency.Token, amount float64, tradeType TradeType, routingConfig AlphaRouterConfig) *BestSwapRoute { + //percents, amount := a.getAmountDistribution(amount, routingConfig) + + return &BestSwapRoute{} +} + +func (a *AlphaRouter) getAmountDistribution(amount float64, routingConfig AlphaRouterConfig) (float64, float64) { + + return 0, 0 +} + +func (a *AlphaRouter) buildTrade(currencyIn currency.Currency, currencyOut currency.Currency, tradeType TradeType, routes Routes) Trade { + + return Trade{} +} + +func (a *AlphaRouter) buildSwapRoute() SwapRoute { return SwapRoute{} } -// -//func (a AlphaRouter) determineCurrencyInOutFromTradeType( -// tradeType TradeType, -// amount fractions.CurrencyAmount, -// quoteCurrency currency.Currency, -//) (currency.Currency, currency.Currency) { -// if tradeType == EXACT_INPUT { -// return amount.Currency, quoteCurrency -// } else { -// return quoteCurrency, amount.Currency -// } -//} +func (a *AlphaRouter) setRouterConfig(routerConfig AlphaRouterConfig, chainId int) AlphaRouterConfig { + return AlphaRouterConfig{} +} diff --git a/core/alpha_router_config.go b/core/alpha_router_config.go new file mode 100644 index 0000000..4d0a9f5 --- /dev/null +++ b/core/alpha_router_config.go @@ -0,0 +1,9 @@ +package core + +type AlphaRouterConfig struct { + v3ProtocolPoolSelection ProtocolPoolSelection + maxSwapsPerPath int + maxSplits int + minSplits int + distributionPercent int +} diff --git a/core/best_swap_route.go b/core/best_swap_route.go new file mode 100644 index 0000000..0138ae7 --- /dev/null +++ b/core/best_swap_route.go @@ -0,0 +1,4 @@ +package core + +type BestSwapRoute struct { +} diff --git a/core/currency/base_currency.go b/core/currency/base_currency.go index e031d53..afd93bc 100644 --- a/core/currency/base_currency.go +++ b/core/currency/base_currency.go @@ -15,7 +15,6 @@ type BaseCurrency struct { address string } -// 이게 필요할까?? func NewBaseCurrency(chainId int64, decimals int64, symbol string, name string) *BaseCurrency { // 아래 코드는 원문 //invariant(Number.isSafeInteger(chainId), 'CHAIN_ID'); diff --git a/core/currency/currency.go b/core/currency/currency.go index d822769..097e8b4 100644 --- a/core/currency/currency.go +++ b/core/currency/currency.go @@ -2,4 +2,5 @@ package currency // Currency는 Token | NativeCurrency type Currency interface { + GetToken() Token } diff --git a/core/currency/native_currency.go b/core/currency/native_currency.go index 746310b..cf106da 100644 --- a/core/currency/native_currency.go +++ b/core/currency/native_currency.go @@ -3,3 +3,7 @@ package currency type NativeCurrency struct { BaseCurrency } + +func (n *NativeCurrency) getToken() Token { + return Token{} // 임시 +} diff --git a/core/currency/token.go b/core/currency/token.go index 31ca4e2..e4988cd 100644 --- a/core/currency/token.go +++ b/core/currency/token.go @@ -4,3 +4,7 @@ type Token struct { BaseCurrency address string } + +func (t *Token) getToken() Token { + return *t +} diff --git a/core/portion_provider.go b/core/portion_provider.go new file mode 100644 index 0000000..b30b5a1 --- /dev/null +++ b/core/portion_provider.go @@ -0,0 +1,8 @@ +package core + +type IPortionProvider interface { + GetPortionAmount(tokenOutAmount float64, tradeType TradeType, swapConfig SwapOptions) (float64, error) +} + +type PortionProvider struct { +} diff --git a/core/protocol_pool_selection.go b/core/protocol_pool_selection.go new file mode 100644 index 0000000..4993b99 --- /dev/null +++ b/core/protocol_pool_selection.go @@ -0,0 +1,14 @@ +package core + +type ProtocolPoolSelection struct { + topN int + topNDirectSwaps int + topNTokenInOut int + topNSecondHop int + topNWithEachBaseToken int + topNWithBaseToken int + + // selectable variable + //topNSecondHopForTokenAddress + //tokensToAvoidOnSecondHops +} diff --git a/core/router.go b/core/router.go index 3ff9580..f4958ae 100644 --- a/core/router.go +++ b/core/router.go @@ -1,6 +1,7 @@ package core type SwapRoute struct { + route Routes } type IRouter interface { diff --git a/core/routes.go b/core/routes.go new file mode 100644 index 0000000..6d73547 --- /dev/null +++ b/core/routes.go @@ -0,0 +1,4 @@ +package core + +type Routes struct { +} diff --git a/core/trade.go b/core/trade.go new file mode 100644 index 0000000..805e0b8 --- /dev/null +++ b/core/trade.go @@ -0,0 +1,6 @@ +package core + +type Trade struct { + //v3Routes V3Routes + tradeType TradeType +} diff --git a/core/types.go b/core/types.go deleted file mode 100644 index 2c40893..0000000 --- a/core/types.go +++ /dev/null @@ -1,9 +0,0 @@ -package core - -// interface는 I 접두사를 붙이는 것이 관행인가? -type IPortionProvider interface { - // GetPortionAmount(tokenOutAmount fractions.CurrencyAmount, tradeType TradeType, swapConfig SwapOptions) fractions.CurrencyAmount -} - -type PortionProvider struct { -} diff --git a/oryxBuildBinary b/oryxBuildBinary new file mode 100644 index 0000000..63f6a45 Binary files /dev/null and b/oryxBuildBinary differ diff --git a/poc/my_router.go b/poc/my_router.go index 4205533..cf8acd9 100644 --- a/poc/my_router.go +++ b/poc/my_router.go @@ -5,58 +5,136 @@ import ( "math" ) +// MyRouter +// router PoC type MyRouter struct { network map[string]*Pool + adj map[string][]string } func NewMyRouter(edges []*Pool) *MyRouter { router := &MyRouter{ network: make(map[string]*Pool), + adj: make(map[string][]string), } for _, edge := range edges { router.network[edge.Address] = edge + router.adj[edge.TokenA.Symbol] = append(router.adj[edge.TokenA.Symbol], edge.TokenB.Symbol) + router.adj[edge.TokenB.Symbol] = append(router.adj[edge.TokenB.Symbol], edge.TokenA.Symbol) } return router } -func (m *MyRouter) Swap(request SwapRequest) (SwapResult, error) { - // poolName은 from:to가 아니라 to:from일 수 있다. - poolName := request.FromToken + ":" + request.ToToken +// Route +// 두 개의 토큰 사이의 효율적인 경로를 계산하는 함수 +func (m *MyRouter) Route(request SwapRequest) ([]SwapResult, error) { + // V1 Router + return m.findRouteV1(request) - if pool, ok := m.network[poolName]; ok { - fmt.Printf("pool found: %v\n", pool) - - reserveFromToken, reserveToToken := m.getReserveOfTokenFromPool(request.FromToken, request.ToToken, *pool) - exchangedAmount := m.calculateAmountOfToToken(reserveFromToken, reserveToToken, request.AmountIn, *pool) - - //saveSwap() - // TODO: 지금은 간이로 코드 작성하고 나중에 함수로 빼든 리팩토링 할 것 - if pool.TokenA.Symbol == request.FromToken { - pool.ReserveA += request.AmountIn - pool.ReserveB += exchangedAmount - } else { - pool.ReserveA += exchangedAmount - pool.ReserveB += request.AmountIn + // V2 Router + //return m.findRouteV2(startTokenSymbol, endTokenSymbol, AmountIn, 1) +} + +// findRouteV1 +// 두 토큰을 direct swap한다 +func (m *MyRouter) findRouteV1(request SwapRequest) ([]SwapResult, error) { + return m.swap(request.FromTokenSymbol, request.ToTokenSymbol, request.AmountIn) +} + +// findRouteV2 +// 경로가 maxLength 이하의 길이인 경로를 탐색해 route를 구한다 +func (m *MyRouter) findRouteV2(request SwapRequest, maxLength int, routes []SwapResult) ([]SwapResult, error) { + startTokenSymbol, beforeTokenSymbol, amountIn := m.setSymbolAndAmountIn(request, routes) + if startTokenSymbol == request.ToTokenSymbol { + return routes, nil + } + if len(routes) >= maxLength { + return nil, fmt.Errorf("the length of routes exceeds maxLength") + } + + var bestPath []SwapResult + for _, toTokenSymbol := range m.adj[startTokenSymbol] { + if toTokenSymbol == beforeTokenSymbol { // 경로 2인 cycle은 허용하지 않음 + continue } - return SwapResult{ - AmountIn: request.AmountIn, - AmountOut: math.Abs(exchangedAmount), - }, nil + route, swapErr := m.swap(startTokenSymbol, toTokenSymbol, amountIn) + if swapErr != nil { + continue + } + + workablePath, findRouteErr := m.findRouteV2(request, maxLength, append(routes, route...)) + if findRouteErr != nil { + continue + } + + if len(workablePath) != 0 && (bestPath == nil || (bestPath[len(bestPath)-1].AmountOut < workablePath[len(workablePath)-1].AmountOut)) { + bestPath = workablePath + } } - return SwapResult{}, fmt.Errorf("pool %s not found", poolName) + return bestPath, nil } -func (m *MyRouter) getReserveOfTokenFromPool(fromTokenName string, toTokenName string, pool Pool) (float64, float64) { - if fromTokenName == pool.TokenA.Symbol { +// setSymbolAndAmountIn +func (m *MyRouter) setSymbolAndAmountIn(request SwapRequest, routes []SwapResult) (string, string, float64) { + if routes == nil { // 처음 함수가 호출된 거라면 + return request.FromTokenSymbol, "", request.AmountIn + } + return routes[len(routes)-1].OutTokenSymbol, routes[len(routes)-1].InTokenSymbol, routes[len(routes)-1].AmountOut +} + +// Swap +// 두 개의 토큰 사이의 직접적인 Pool을 통해 두 개의 토큰을 교환하는 함수 +func (m *MyRouter) swap(fromTokenSymbol string, toTokenSymbol string, amountIn float64) ([]SwapResult, error) { + // TODO: poolName은 from:to가 아니라 to:from일 수 있다. + // TODO: 문자열 연산 최적화 + poolName := fromTokenSymbol + ":" + toTokenSymbol + + if pool, ok := m.network[poolName]; ok { + reserveFromToken, reserveToToken := m.getReserveOfTokenFromPool(fromTokenSymbol, toTokenSymbol, *pool) + amountOut := m.calculateAmountOfToToken(reserveFromToken, reserveToToken, amountIn, *pool) + + // 같은 경로를 두 번 이상 탐색하지 않으므로 일단 주석 처리 + //m.saveSwap(fromTokenSymbol, amountIn, amountOut, pool) + + return []SwapResult{{ + InTokenSymbol: fromTokenSymbol, + OutTokenSymbol: toTokenSymbol, + AmountIn: amountIn, + AmountOut: math.Abs(amountOut), + }}, nil + } + + return nil, fmt.Errorf("pool %s not found", poolName) +} + +// saveSwap +func (m *MyRouter) saveSwap(fromTokenSymbol string, amountIn, amountOut float64, pool *Pool) { + if pool.TokenA.Symbol == fromTokenSymbol { + pool.ReserveA += amountIn + pool.ReserveB += amountOut + } else { + pool.ReserveA += amountOut + pool.ReserveB += amountIn + } +} + +// getReserveOfTokenFromPool +// Pool에 있는 fromToken과 toToken의 reserve 쌍을 반환하는 함수 +func (m *MyRouter) getReserveOfTokenFromPool(fromTokenSymbol string, toTokenSymbol string, pool Pool) (float64, float64) { + if fromTokenSymbol == pool.TokenA.Symbol { return pool.ReserveA, pool.ReserveB } return pool.ReserveB, pool.ReserveA } +// calculateAmountOfToToken +// 토큰이 교환될 때 교환자에게 지급해야 할 toToken의 양을 계산하는 함수 +// 계산 과정 최적화 하면 곱셈 5번, 덧셈 2번 정도에 해결 가능함 +// ref: https://hyun-jeong.medium.com/uniswap-series-2-cpmm-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-4a82de8aba9 func (m *MyRouter) calculateAmountOfToToken(reserveFromToken, reserveToToken, amountIn float64, pool Pool) float64 { X := reserveFromToken Y := reserveToToken @@ -74,10 +152,5 @@ func (m *MyRouter) calculateAmountOfToToken(reserveFromToken, reserveToToken, am // X 코인이 dX개 만큼 증가했을 때 // Y 코인은 dY개 만큼 감소해야 한다. // X -> X + dX, Y -> Y + dY - return dY } - -func (m *MyRouter) dijskrtra() { - -} diff --git a/poc/my_router_test.go b/poc/my_router_test.go index 8b869ea..8cb31eb 100644 --- a/poc/my_router_test.go +++ b/poc/my_router_test.go @@ -6,26 +6,60 @@ import ( "testing" ) -func TestMyRouter(t *testing.T) { +const tolerance = 0.00000001 // 오차 범위 + +func TestMyRouterV1(t *testing.T) { tokens := map[string]Token{ - "a": Token{Symbol: "a"}, - "b": Token{Symbol: "b"}, + "a": {Symbol: "a"}, + "b": {Symbol: "b"}, } tests := []struct { + name string edges []*Pool requests []SwapRequest results []SwapResult }{ { - []*Pool{ - {"a:b", tokens["a"], tokens["b"], 4000, 1000}}, - []SwapRequest{ - {"a", "b", 2000}}, - []SwapResult{ - {2000.0, 2000.0 / 6.0}, + name: "단일 홉 스왑", + edges: []*Pool{ + {"a:b", tokens["a"], tokens["b"], 4000, 1000}, + }, + requests: []SwapRequest{ + {"a", "b", 2000}, + }, + results: []SwapResult{ + {"a", "b", 2000.0, 2000.0 / 6.0}, }, }, + // TODO: 아래 테스트 케이스도 통과해야 함. 값은 검증 필요. + // { + // name: "극단적인 비율 스왑", + // edges: []*Pool{ + // {"a:b", tokens["a"], tokens["b"], 1000000, 1}, + // }, + // requests: []SwapRequest{ + // {"a", "b", 500}, + // }, + // results: []SwapResult{ + // {"a", "b", 500, 0.0004999999999999999}, + // }, + // }, + // { + // name: "양방향 스왑", + // edges: []*Pool{ + // {"a:b", tokens["a"], tokens["b"], 4000, 1000}, + // {"b:a", tokens["b"], tokens["a"], 1000, 4000}, + // }, + // requests: []SwapRequest{ + // {"a", "b", 2000}, + // {"b", "a", 500}, + // }, + // results: []SwapResult{ + // {"a", "b", 2000, 2000.0 / 6.0}, + // {"b", "a", 500, 500.0 / 6.0}, + // }, + // }, } for _, test := range tests { @@ -33,20 +67,118 @@ func TestMyRouter(t *testing.T) { router := NewMyRouter(test.edges) for i, request := range test.requests { - result, err := router.Swap(request) + result, err := router.findRouteV1(request) if err != nil { - t.Fatalf("Swap Error: can't find pool: %v:%v", request.FromToken, request.ToToken) + t.Fatalf("Swap Error: can't find pool: %v:%v", request.FromTokenSymbol, request.ToTokenSymbol) } - diff := math.Abs(result.AmountOut - test.results[i].AmountOut) - tolerance := 0.00000001 + diff := math.Abs(result[0].AmountOut - test.results[i].AmountOut) if diff > tolerance { - t.Fatalf("Swap: Unexpected Token output number, expected: %v, got %v", test.results[i].AmountOut, result.AmountOut) + t.Fatalf("Swap: Unexpected Token output number, expected: %v, got %v", test.results[i].AmountOut, result[0].AmountOut) } - fmt.Println(result) + fmt.Println(result[0]) + fmt.Println("스왑 결과") for _, pool := range router.network { - fmt.Println(pool) + fmt.Printf("pool (%s) %s: %f %s: %f\n", pool.Address, pool.TokenA.Symbol, pool.ReserveA, pool.TokenB.Symbol, pool.ReserveB) + } + } + }) + } +} + +func TestMyRouterV2(t *testing.T) { + tokens := map[string]Token{ + "a": {Symbol: "a"}, + "b": {Symbol: "b"}, + "c": {Symbol: "c"}, + "d": {Symbol: "d"}, + } + + tests := []struct { + name string + edges []*Pool + requests []SwapRequest + results []SwapResult + maxSearchLength int + expectError bool + }{ + { + name: "최대 검색 길이 1의 다중 홉 스왑", + edges: []*Pool{ + {"a:b", tokens["a"], tokens["b"], 4000, 1000}, + {"a:c", tokens["a"], tokens["c"], 2000, 1000}, + {"b:c", tokens["b"], tokens["c"], 2000, 4000}}, + requests: []SwapRequest{ + {"a", "c", 2000}}, + results: []SwapResult{ + {"a", "c", 2000, 571.4285714285}, + }, + maxSearchLength: 2, + }, + { + name: "최대 검색 길이 2의 다중 홉 스왑", + edges: []*Pool{ + {"a:b", tokens["a"], tokens["b"], 4000, 1000}, + {"a:c", tokens["a"], tokens["c"], 2000, 1000}, + {"b:c", tokens["b"], tokens["c"], 2000, 4000}, + }, + requests: []SwapRequest{ + {"a", "c", 2000}, + }, + results: []SwapResult{ + {"a", "c", 2000, 500}, + }, + maxSearchLength: 1, + }, + { + name: "다양한 경로 스왑", + edges: []*Pool{ + {"a:b", tokens["a"], tokens["b"], 4000, 1000}, + {"b:c", tokens["b"], tokens["c"], 2000, 4000}, + {"a:c", tokens["a"], tokens["c"], 2000, 1000}, + {"c:d", tokens["c"], tokens["d"], 1000, 500}, + }, + requests: []SwapRequest{ + {"a", "d", 1000}, + }, + results: []SwapResult{ + {"a", "d", 1000, 133.33333333333334}, // TODO: 임의로 넣은 값이라 검증 필요 + }, + maxSearchLength: 3, + }, + { + name: "검색 길이가 음수인 경우", + edges: []*Pool{ + {"a:b", tokens["a"], tokens["b"], 4000, 1000}, + {"b:c", tokens["b"], tokens["c"], 2000, 4000}, + {"c:d", tokens["c"], tokens["d"], 1000, 500}, + {"a:d", tokens["a"], tokens["d"], 3000, 1500}, + }, + requests: []SwapRequest{ + {"a", "d", 1500}, + }, + maxSearchLength: -1, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + router := NewMyRouter(test.edges) + + for i, request := range test.requests { + result, err := router.findRouteV2(request, test.maxSearchLength, nil) + if err != nil { + if !test.expectError { + t.Fatalf("Router: can't find path: %v:%v", request.FromTokenSymbol, request.ToTokenSymbol) + } + continue + } + + diff := math.Abs(result[len(result)-1].AmountOut - test.results[i].AmountOut) + if diff > tolerance { + t.Fatalf("Router: Unexpected Token output number, expected: %v, got %v", test.results[i].AmountOut, result[len(result)-1].AmountOut) } } }) diff --git a/poc/pool.go b/poc/pool.go index 12c4707..202f01e 100644 --- a/poc/pool.go +++ b/poc/pool.go @@ -1,11 +1,9 @@ package poc -// Pool -// kind of edge type Pool struct { Address string - TokenA Token // 효율을 위해서는 포인터로 가져오는게 좋을만 하다. + TokenA Token // Todo: 효율을 위해서는 포인터로 가져오는게 좋을만 하다. TokenB Token ReserveA float64 diff --git a/poc/swap_request.go b/poc/swap_request.go index af13e08..31e2ba2 100644 --- a/poc/swap_request.go +++ b/poc/swap_request.go @@ -1,9 +1,9 @@ package poc type SwapRequest struct { - FromToken string - ToToken string - AmountIn float64 + FromTokenSymbol string + ToTokenSymbol string + AmountIn float64 //MinAmountOut int - //UserAddress string // option + //UserAddress string // optional } diff --git a/poc/swap_result.go b/poc/swap_result.go index 22bfb71..1ba3ab3 100644 --- a/poc/swap_result.go +++ b/poc/swap_result.go @@ -1,6 +1,8 @@ package poc type SwapResult struct { - AmountIn float64 - AmountOut float64 + InTokenSymbol string + OutTokenSymbol string + AmountIn float64 + AmountOut float64 }