Skip to content

Commit cf32f73

Browse files
committed
feat: add support for javascript binding
1 parent 2258a6b commit cf32f73

File tree

28 files changed

+583
-20
lines changed

28 files changed

+583
-20
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,11 @@ jobs:
190190
if ("$ENV{GITHUB_EVENT_NAME}" STREQUAL "push")
191191
set(ENV{CMAKE_BUILD_TYPE} "Release")
192192
set(ENV{DOTNET_BUILD_CONFIGURATION} "Release")
193+
set(ENV{NODE_GYP_BUILD_MODE} "release")
193194
else()
194195
set(ENV{CMAKE_BUILD_TYPE} "Debug")
195196
set(ENV{DOTNET_BUILD_CONFIGURATION} "Debug")
197+
set(ENV{NODE_GYP_BUILD_MODE} "debug")
196198
endif()
197199
198200
execute_process(
@@ -233,11 +235,14 @@ jobs:
233235
-DBUILD_PYTHON=ON
234236
-DBUILD_JAVA=ON
235237
-DBUILD_CSHARP=ON
238+
-DBUILD_GO=ON
239+
-DBUILD_JAVASCRIPT=ON
236240
-DBUILD_TEST=ON
237241
-S binding
238242
-B binding/build
239243
-D CMAKE_BUILD_TYPE=$ENV{CMAKE_BUILD_TYPE}
240244
-D DOTNET_BUILD_CONFIGURATION=$ENV{DOTNET_BUILD_CONFIGURATION}
245+
-D DNODE_GYP_BUILD_MODE=$ENV{NODE_GYP_BUILD_MODE}
241246
-G Ninja
242247
-D CMAKE_MAKE_PROGRAM=ninja
243248
# -D CMAKE_C_COMPILER_LAUNCHER=ccache

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,5 @@ docs/_build/
150150
.venv/
151151
target/
152152
obj/
153+
node_modules/
154+
package-lock.json

binding/csharp/example/fix_simple/MainProgram.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public override bool ProcessEvent(ccapi.Event event_, ccapi.Session session) {
1616
param.Add(new ccapi.PairIntString(40, "2"));
1717
param.Add(new ccapi.PairIntString(59, "1"));
1818
request.AppendParamFix(param);
19-
session.SendRequest(request);
19+
session.SendRequestByFix(request);
2020
}
2121
} else if (event_.GetType_() == ccapi.Event.Type.FIX) {
2222
System.Console.WriteLine(string.Format("Received an event of type FIX:\n{0}", event_.ToStringPretty(2, 2)));

binding/go/example/fix_simple/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func (*MyEventHandler) ProcessEvent(event ccapi.Event, session ccapi.Session) bo
4141
param.Add(aParam)
4242
}
4343
request.AppendParamFix(param)
44-
session.SendRequest(request)
44+
session.SendRequestByFix(request)
4545
}
4646
} else if event.GetType() == ccapi.EventType_FIX {
4747
fmt.Printf("Received an event of type FIX:\n%s\n", event.ToStringPretty(2, 2))

binding/go/example/market_data_simple_subscription/main.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@ func (*MyEventHandler) ProcessEvent(event ccapi.Event, session ccapi.Session) bo
2121
elementList := message.GetElementList()
2222
for j := 0; j < int(elementList.Size()); j++ {
2323
element := elementList.Get(j)
24-
nameValueMap := element.GetNameValueMap()
25-
if nameValueMap.Has_key("BID_PRICE") {
26-
fmt.Printf(" %s = %s\n", "BID_PRICE", nameValueMap.Get("BID_PRICE"))
24+
elementNameValueMap := element.GetNameValueMap()
25+
if elementNameValueMap.Has_key("BID_PRICE") {
26+
fmt.Printf(" BID_PRICE = %s\n", elementNameValueMap.Get("BID_PRICE"))
2727
}
28-
if nameValueMap.Has_key("BID_SIZE") {
29-
fmt.Printf(" %s = %s\n", "BID_SIZE", nameValueMap.Get("BID_SIZE"))
28+
if elementNameValueMap.Has_key("BID_SIZE") {
29+
fmt.Printf(" BID_SIZE = %s\n", elementNameValueMap.Get("BID_SIZE"))
3030
}
31-
if nameValueMap.Has_key("ASK_PRICE") {
32-
fmt.Printf(" %s = %s\n", "ASK_PRICE", nameValueMap.Get("ASK_PRICE"))
31+
if elementNameValueMap.Has_key("ASK_PRICE") {
32+
fmt.Printf(" ASK_PRICE = %s\n", elementNameValueMap.Get("ASK_PRICE"))
3333
}
34-
if nameValueMap.Has_key("ASK_SIZE") {
35-
fmt.Printf(" %s = %s\n", "ASK_SIZE", nameValueMap.Get("ASK_SIZE"))
34+
if elementNameValueMap.Has_key("ASK_SIZE") {
35+
fmt.Printf(" ASK_SIZE = %s\n", elementNameValueMap.Get("ASK_SIZE"))
3636
}
3737
}
3838
}

binding/go/test/go.mod

Lines changed: 0 additions & 7 deletions
This file was deleted.

binding/java/example/fix_simple/Main.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public boolean processEvent(Event event, Session session) {
2929
param.add(new PairIntString(40, "2"));
3030
param.add(new PairIntString(59, "1"));
3131
request.appendParamFix(param);
32-
session.sendRequest(request);
32+
session.sendRequestByFix(request);
3333
}
3434
} else if (event.getType() == Event.Type.FIX) {
3535
System.out.println(String.format("Received an event of type FIX:\n%s", event.toStringPretty(2, 2)));

binding/javascript/CMakeLists.txt

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
set(NAME binding_${LANG})
2+
project(${NAME})
3+
set(SWIG_TARGET_NAME ccapi_${NAME})
4+
5+
# Find javascript cli
6+
find_program(NODE_EXECUTABLE NAMES node REQUIRED)
7+
if(NOT NODE_EXECUTABLE)
8+
message(FATAL_ERROR "Check for node Program: not found")
9+
else()
10+
message(STATUS "Found node Program: ${NODE_EXECUTABLE}")
11+
endif()
12+
13+
execute_process(
14+
COMMAND ${NODE_EXECUTABLE} -v
15+
OUTPUT_VARIABLE NODE_EXECUTABLE_VERSION
16+
OUTPUT_STRIP_TRAILING_WHITESPACE
17+
)
18+
message(STATUS "node version: ${NODE_EXECUTABLE_VERSION}")
19+
20+
find_program(NODE_GYP_EXECUTABLE NAMES node-gyp REQUIRED)
21+
if(NOT NODE_GYP_EXECUTABLE)
22+
message(FATAL_ERROR "Check for node-gyp Program: not found")
23+
else()
24+
message(STATUS "Found node-gyp Program: ${NODE_EXECUTABLE}")
25+
endif()
26+
27+
execute_process(
28+
COMMAND ${NODE_GYP_EXECUTABLE} -v
29+
OUTPUT_VARIABLE NODE_GYP_EXECUTABLE_VERSION
30+
OUTPUT_STRIP_TRAILING_WHITESPACE
31+
)
32+
message(STATUS "node-gyp version: ${NODE_GYP_EXECUTABLE_VERSION}")
33+
34+
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${SWIG_TARGET_NAME})
35+
set(BINDING_GYP_CFLAGS_CC_LIST "")
36+
set(BINDING_GYP_LDFLAGS_LIST "")
37+
set(BINDING_GYP_DEFINES_LIST "")
38+
list(APPEND BINDING_GYP_CFLAGS_CC_LIST -std=c++17 -fPIC -Wno-deprecated-declarations)
39+
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
40+
list(APPEND BINDING_GYP_CFLAGS_CC_LIST -g)
41+
list(APPEND BINDING_GYP_LDFLAGS_LIST -g)
42+
else()
43+
list(APPEND BINDING_GYP_CFLAGS_CC_LIST -O3 -DNDEBUG)
44+
endif()
45+
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
46+
list(APPEND BINDING_GYP_CFLAGS_CC_LIST -pthread)
47+
endif()
48+
list(APPEND BINDING_GYP_DEFINES_LIST CCAPI_ENABLE_SERVICE_MARKET_DATA CCAPI_ENABLE_SERVICE_EXECUTION_MANAGEMENT CCAPI_ENABLE_SERVICE_FIX CCAPI_ENABLE_EXCHANGE_COINBASE CCAPI_ENABLE_EXCHANGE_GEMINI CCAPI_ENABLE_EXCHANGE_KRAKEN CCAPI_ENABLE_EXCHANGE_KRAKEN_FUTURES CCAPI_ENABLE_EXCHANGE_BITSTAMP CCAPI_ENABLE_EXCHANGE_BITFINEX CCAPI_ENABLE_EXCHANGE_BITMEX CCAPI_ENABLE_EXCHANGE_BINANCE_US CCAPI_ENABLE_EXCHANGE_BINANCE CCAPI_ENABLE_EXCHANGE_BINANCE_MARGIN CCAPI_ENABLE_EXCHANGE_BINANCE_USDS_FUTURES CCAPI_ENABLE_EXCHANGE_BINANCE_COIN_FUTURES CCAPI_ENABLE_EXCHANGE_HUOBI CCAPI_ENABLE_EXCHANGE_HUOBI_USDT_SWAP CCAPI_ENABLE_EXCHANGE_HUOBI_COIN_SWAP CCAPI_ENABLE_EXCHANGE_OKX CCAPI_ENABLE_EXCHANGE_ERISX CCAPI_ENABLE_EXCHANGE_KUCOIN CCAPI_ENABLE_EXCHANGE_KUCOIN_FUTURES CCAPI_ENABLE_EXCHANGE_DERIBIT CCAPI_ENABLE_EXCHANGE_GATEIO CCAPI_ENABLE_EXCHANGE_GATEIO_PERPETUAL_FUTURES CCAPI_ENABLE_EXCHANGE_CRYPTOCOM CCAPI_ENABLE_EXCHANGE_BYBIT CCAPI_ENABLE_EXCHANGE_BYBIT_DERIVATIVES CCAPI_ENABLE_EXCHANGE_ASCENDEX CCAPI_ENABLE_EXCHANGE_BITGET CCAPI_ENABLE_EXCHANGE_BITGET_FUTURES CCAPI_ENABLE_EXCHANGE_BITMART CCAPI_ENABLE_EXCHANGE_MEXC CCAPI_ENABLE_EXCHANGE_MEXC_FUTURES CCAPI_ENABLE_EXCHANGE_WHITEBIT)
49+
if(CCAPI_ENABLE_LOG_ERROR)
50+
list(APPEND BINDING_GYP_DEFINES_LIST CCAPI_ENABLE_LOG_ERROR)
51+
endif()
52+
if(CCAPI_ENABLE_LOG_WARN)
53+
list(APPEND BINDING_GYP_DEFINES_LIST CCAPI_ENABLE_LOG_WARN)
54+
endif()
55+
if(CCAPI_ENABLE_LOG_INFO)
56+
list(APPEND BINDING_GYP_DEFINES_LIST CCAPI_ENABLE_LOG_INFO)
57+
endif()
58+
if(CCAPI_ENABLE_LOG_DEBUG)
59+
list(APPEND BINDING_GYP_DEFINES_LIST CCAPI_ENABLE_LOG_DEBUG)
60+
endif()
61+
if(CCAPI_ENABLE_LOG_TRACE)
62+
list(APPEND BINDING_GYP_DEFINES_LIST CCAPI_ENABLE_LOG_TRACE)
63+
endif()
64+
list(JOIN BINDING_GYP_CFLAGS_CC_LIST "\",\"" BINDING_GYP_CFLAGS_CC)
65+
message(STATUS "BINDING_GYP_CFLAGS_CC: ${BINDING_GYP_CFLAGS_CC}")
66+
list(JOIN BINDING_GYP_LDFLAGS_LIST "\",\"" BINDING_GYP_LDFLAGS)
67+
message(STATUS "BINDING_GYP_LDFLAGS: ${BINDING_GYP_LDFLAGS}")
68+
list(JOIN BINDING_GYP_DEFINES_LIST "\",\"" BINDING_GYP_DEFINES)
69+
message(STATUS "BINDING_GYP_DEFINES: ${BINDING_GYP_DEFINES}")
70+
71+
configure_file(
72+
${CMAKE_CURRENT_SOURCE_DIR}/binding.gyp.in
73+
${CMAKE_CURRENT_BINARY_DIR}/${SWIG_TARGET_NAME}/binding.gyp
74+
@ONLY)
75+
if(NOT NODE_GYP_BUILD_MODE)
76+
set(NODE_GYP_BUILD_MODE "release" CACHE STRING
77+
"Choose the type of build, options are: debug, release."
78+
FORCE)
79+
endif()
80+
if("${NODE_GYP_BUILD_MODE}" STREQUAL "release")
81+
set(NODE_GYP_BUILD_DIR "build/Release")
82+
else()
83+
set(NODE_GYP_BUILD_DIR "build/Debug")
84+
endif()
85+
message(STATUS "NODE_GYP_BUILD_MODE: ${NODE_GYP_BUILD_MODE}")
86+
add_custom_target(${SWIG_TARGET_NAME} ALL
87+
COMMAND ${SWIG_EXECUTABLE} -outcurrentdir -c++ -javascript -node -I${CCAPI_PROJECT_DIR}/include ${SWIG_INTERFACE}
88+
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/ccapi_logger.cpp .
89+
COMMAND ${NODE_GYP_EXECUTABLE} --${NODE_GYP_BUILD_MODE} clean configure build
90+
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${SWIG_TARGET_NAME}
91+
)
92+
add_dependencies(${SWIG_TARGET_NAME} boost rapidjson hffix)
93+
94+
set(PACKAGING_DIR packaging)
95+
set(PACKAGING_DIR_FULL ${CMAKE_CURRENT_BINARY_DIR}/${PACKAGING_DIR}/${BUILD_VERSION})
96+
file(MAKE_DIRECTORY ${PACKAGING_DIR_FULL})
97+
set(JAVASCRIPT_PACKAGING_TARGET_NAME ${LANG}_${PACKAGING_DIR})
98+
configure_file(
99+
${CMAKE_CURRENT_SOURCE_DIR}/package.json.in
100+
${PACKAGING_DIR_FULL}/package.json
101+
@ONLY)
102+
add_custom_target(${JAVASCRIPT_PACKAGING_TARGET_NAME} ALL
103+
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/${SWIG_TARGET_NAME}/${NODE_GYP_BUILD_DIR}/index.node ${PACKAGING_DIR_FULL}
104+
WORKING_DIRECTORY ${PACKAGING_DIR_FULL}
105+
)
106+
add_dependencies(${JAVASCRIPT_PACKAGING_TARGET_NAME} ${SWIG_TARGET_NAME})
107+
108+
# Test
109+
if(BUILD_TEST)
110+
set(TEST_DIR ${CMAKE_CURRENT_BINARY_DIR}/test)
111+
find_program(NPM_EXECUTABLE NAMES npm REQUIRED)
112+
if(NOT NPM_EXECUTABLE)
113+
message(FATAL_ERROR "Check for npm Program: not found")
114+
else()
115+
message(STATUS "Found npm Program: ${NPM_EXECUTABLE}")
116+
endif()
117+
file(COPY test DESTINATION ${TEST_DIR})
118+
set(JAVASCRIPT_TEST_TARGET_NAME javascript_test_test)
119+
set(JAVASCRIPT_TEST_BUILD_DIRECTORY ${TEST_DIR}/test)
120+
file(MAKE_DIRECTORY ${JAVASCRIPT_TEST_BUILD_DIRECTORY})
121+
add_custom_target(${JAVASCRIPT_TEST_TARGET_NAME} ALL
122+
COMMAND ${CMAKE_COMMAND} -E rm -rf node_modules
123+
COMMAND ${NPM_EXECUTABLE} install ${PACKAGING_DIR_FULL}
124+
WORKING_DIRECTORY ${JAVASCRIPT_TEST_BUILD_DIRECTORY}
125+
)
126+
add_dependencies(${JAVASCRIPT_TEST_TARGET_NAME} ${JAVASCRIPT_PACKAGING_TARGET_NAME})
127+
add_test(NAME javascript_test
128+
COMMAND ${NODE_EXECUTABLE} index.js
129+
WORKING_DIRECTORY ${JAVASCRIPT_TEST_BUILD_DIRECTORY})
130+
endif()

binding/javascript/binding.gyp.in

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"targets": [
3+
{
4+
"target_name": "index",
5+
"sources": [
6+
"swig_interface_wrap.cxx",
7+
"ccapi_logger.cpp"
8+
],
9+
"include_dirs": [
10+
"@CCAPI_PROJECT_DIR@/include",
11+
"@BOOST_INCLUDE_DIR@",
12+
"@RAPIDJSON_INCLUDE_DIR@",
13+
"@HFFIX_INCLUDE_DIR@",
14+
"<(node_root_dir)/deps/openssl/openssl/include"
15+
],
16+
"cflags_cc!": [
17+
"-fno-exceptions"
18+
],
19+
"conditions": [
20+
[
21+
"OS=='mac'",
22+
{
23+
"xcode_settings": {
24+
"OTHER_CPLUSPLUSFLAGS" : [ "@BINDING_GYP_CFLAGS_CC@" ],
25+
"GCC_ENABLE_CPP_EXCEPTIONS": "YES"
26+
}
27+
}
28+
],
29+
[
30+
"target_arch=='ia32'",
31+
{
32+
"include_dirs": [
33+
"<(node_root_dir)/deps/openssl/config/piii"
34+
]
35+
}
36+
],
37+
[
38+
"target_arch=='x64'",
39+
{
40+
"include_dirs": [
41+
"<(node_root_dir)/deps/openssl/config/k8"
42+
]
43+
}
44+
],
45+
[
46+
"target_arch=='arm'",
47+
{
48+
"include_dirs": [
49+
"<(node_root_dir)/deps/openssl/config/arm"
50+
]
51+
}
52+
]
53+
],
54+
"cflags_cc": [
55+
"@BINDING_GYP_CFLAGS_CC@"
56+
],
57+
"ldflags": [
58+
"@BINDING_GYP_LDFLAGS@"
59+
],
60+
"defines": [
61+
"@BINDING_GYP_DEFINES@"
62+
]
63+
}
64+
]
65+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const ccapi = require('ccapi')
2+
if (!process.env.OKX_API_KEY) {
3+
console.error('Please set environment variable OKX_API_KEY')
4+
process.exit(1)
5+
}
6+
if (!process.env.OKX_API_SECRET) {
7+
console.error('Please set environment variable OKX_API_SECRET')
8+
process.exit(1)
9+
}
10+
if (!process.env.OKX_API_PASSPHRASE) {
11+
console.error('Please set environment variable OKX_API_PASSPHRASE')
12+
process.exit(1)
13+
}
14+
const Session = ccapi.Session
15+
const SessionConfigs = ccapi.SessionConfigs
16+
const SessionOptions = ccapi.SessionOptions
17+
const Request = ccapi.Request
18+
const MapStringString = ccapi.MapStringString
19+
const sessionConfigs = new SessionConfigs()
20+
const sessionOptions = new SessionOptions()
21+
const session = new Session(sessionOptions, sessionConfigs)
22+
const request = new Request(Request.Operation_CREATE_ORDER, 'okx', 'BTC-USDT')
23+
const param = new MapStringString()
24+
param.set('SIDE', 'BUY')
25+
param.set('QUANTITY', '0.0005')
26+
param.set('LIMIT_PRICE', '20000')
27+
request.appendParam(param)
28+
session.sendRequest(request)
29+
const intervalId = setInterval(() => {
30+
const eventList = session.getEventQueue().purge()
31+
for (let i = 0; i < eventList.size(); i++) {
32+
const event = eventList.get(i)
33+
console.log(`Received an event:\n${event.toStringPretty(2, 2)}`)
34+
}
35+
}, 1)
36+
setTimeout(() => {
37+
clearInterval(intervalId)
38+
session.stop()
39+
console.log('Bye')
40+
}, 10000)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"ccapi": "file:../../../build/javascript/packaging/1.0.0"
4+
}
5+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const ccapi = require('ccapi')
2+
if (!process.env.OKX_API_KEY) {
3+
console.error('Please set environment variable OKX_API_KEY')
4+
process.exit(1)
5+
}
6+
if (!process.env.OKX_API_SECRET) {
7+
console.error('Please set environment variable OKX_API_SECRET')
8+
process.exit(1)
9+
}
10+
if (!process.env.OKX_API_PASSPHRASE) {
11+
console.error('Please set environment variable OKX_API_PASSPHRASE')
12+
process.exit(1)
13+
}
14+
const Session = ccapi.Session
15+
const SessionConfigs = ccapi.SessionConfigs
16+
const SessionOptions = ccapi.SessionOptions
17+
const Subscription = ccapi.Subscription
18+
const Request = ccapi.Request
19+
const MapStringString = ccapi.MapStringString
20+
const Event = ccapi.Event
21+
const Message = ccapi.Message
22+
const sessionConfigs = new SessionConfigs()
23+
const sessionOptions = new SessionOptions()
24+
const session = new Session(sessionOptions, sessionConfigs)
25+
const subscription = new Subscription('okx', 'BTC-USDT', 'ORDER_UPDATE')
26+
session.subscribe(subscription)
27+
const intervalId = setInterval(() => {
28+
const eventList = session.getEventQueue().purge()
29+
for (let i = 0; i < eventList.size(); i++) {
30+
const event = eventList.get(i)
31+
if (event.getType() == Event.Type_SUBSCRIPTION_STATUS) {
32+
console.log(`Received an event of type SUBSCRIPTION_STATUS:\n${event.toStringPretty(2, 2)}`)
33+
const message = event.getMessageList().get(0)
34+
if (message.getType() == Message.Type_SUBSCRIPTION_STARTED) {
35+
const request = new Request(Request.Operation_CREATE_ORDER, 'okx', 'BTC-USDT')
36+
const param = new MapStringString()
37+
param.set('SIDE', 'BUY')
38+
param.set('QUANTITY', '0.0005')
39+
param.set('LIMIT_PRICE', '20000')
40+
request.appendParam(param)
41+
session.sendRequest(request)
42+
}
43+
}
44+
else if (event.getType() == ccapi.Event.Type_SUBSCRIPTION_DATA) {
45+
console.log(`Received an event of type SUBSCRIPTION_DATA:\n${event.toStringPretty(2, 2)}`)
46+
}
47+
}
48+
}, 1)
49+
setTimeout(() => {
50+
clearInterval(intervalId)
51+
session.stop()
52+
console.log('Bye')
53+
}, 10000)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"ccapi": "file:../../../build/javascript/packaging/1.0.0"
4+
}
5+
}

0 commit comments

Comments
 (0)