@starport/client-js
Advanced tools
Comparing version 0.1.12 to 0.1.13
@@ -20,2 +20,6 @@ "use strict"; | ||
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } | ||
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } | ||
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } | ||
@@ -79,2 +83,4 @@ | ||
_defineProperty(_assertThisInitialized(_this), "signer", void 0); | ||
_defineProperty(_assertThisInitialized(_this), "timer", void 0); | ||
@@ -128,4 +134,5 @@ | ||
this.signingClient = _context.sent; | ||
this.signer = signer; | ||
case 3: | ||
case 4: | ||
case "end": | ||
@@ -145,5 +152,12 @@ return _context.stop(); | ||
}, { | ||
key: "connectivityTest", | ||
key: "switchAPI", | ||
value: function switchAPI(apiAddr) { | ||
this.apiAddr = apiAddr; | ||
} | ||
}, { | ||
key: "switchWS", | ||
value: function () { | ||
var _connectivityTest = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() { | ||
var _switchWS = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2(wsAddr) { | ||
var _this2 = this; | ||
return regeneratorRuntime.wrap(function _callee2$(_context2) { | ||
@@ -153,9 +167,92 @@ while (1) { | ||
case 0: | ||
this.emit("ws-status", false); | ||
this.wsAddr = wsAddr; | ||
this.connectedPromise = new Promise(function (res, rej) { | ||
_this2.connectRes = res; | ||
_this2.connectRej = rej; | ||
}); | ||
_context2.prev = 3; | ||
this.socket = new _reconnectingWebsocket["default"](this.wsAddr); | ||
_context2.next = 11; | ||
break; | ||
case 7: | ||
_context2.prev = 7; | ||
_context2.t0 = _context2["catch"](3); | ||
this.connectRej(); | ||
throw "WS node unavailable"; | ||
case 11: | ||
this.socket.onopen = this.onOpenWS.bind(this); | ||
this.socket.onmessage = this.onMessageWS.bind(this); | ||
this.socket.onerror = this.onErrorWS.bind(this); | ||
this.socket.onclose = this.onCloseWS.bind(this); | ||
return _context2.abrupt("return", this.connectedPromise); | ||
case 16: | ||
case "end": | ||
return _context2.stop(); | ||
} | ||
} | ||
}, _callee2, this, [[3, 7]]); | ||
})); | ||
function switchWS(_x2) { | ||
return _switchWS.apply(this, arguments); | ||
} | ||
return switchWS; | ||
}() | ||
}, { | ||
key: "switchRPC", | ||
value: function () { | ||
var _switchRPC = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3(rpcAddr) { | ||
var registry; | ||
return regeneratorRuntime.wrap(function _callee3$(_context3) { | ||
while (1) { | ||
switch (_context3.prev = _context3.next) { | ||
case 0: | ||
this.rpcAddr = rpcAddr; | ||
if (!this.signingClient) { | ||
_context3.next = 6; | ||
break; | ||
} | ||
registry = _objectSpread({}, this.signingClient.registry); | ||
_context3.next = 5; | ||
return _stargate.SigningStargateClient.connectWithSigner(this.rpcAddr, this.signer, registry); | ||
case 5: | ||
this.signingClient = _context3.sent; | ||
case 6: | ||
case "end": | ||
return _context3.stop(); | ||
} | ||
} | ||
}, _callee3, this); | ||
})); | ||
function switchRPC(_x3) { | ||
return _switchRPC.apply(this, arguments); | ||
} | ||
return switchRPC; | ||
}() | ||
}, { | ||
key: "connectivityTest", | ||
value: function () { | ||
var _connectivityTest = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee4() { | ||
return regeneratorRuntime.wrap(function _callee4$(_context4) { | ||
while (1) { | ||
switch (_context4.prev = _context4.next) { | ||
case 0: | ||
if (!this.apiAddr) { | ||
_context2.next = 10; | ||
_context4.next = 10; | ||
break; | ||
} | ||
_context2.prev = 1; | ||
_context2.next = 4; | ||
_context4.prev = 1; | ||
_context4.next = 4; | ||
return _axios["default"].get(this.apiAddr + "/node_info"); | ||
@@ -165,10 +262,10 @@ | ||
this.emit("api-status", true); | ||
_context2.next = 10; | ||
_context4.next = 10; | ||
break; | ||
case 7: | ||
_context2.prev = 7; | ||
_context2.t0 = _context2["catch"](1); | ||
_context4.prev = 7; | ||
_context4.t0 = _context4["catch"](1); | ||
if (!_context2.t0.response) { | ||
if (!_context4.t0.response) { | ||
this.emit("api-status", false); | ||
@@ -181,8 +278,8 @@ } else { | ||
if (!this.rpcAddr) { | ||
_context2.next = 20; | ||
_context4.next = 20; | ||
break; | ||
} | ||
_context2.prev = 11; | ||
_context2.next = 14; | ||
_context4.prev = 11; | ||
_context4.next = 14; | ||
return _axios["default"].get(this.rpcAddr); | ||
@@ -192,10 +289,10 @@ | ||
this.emit("rpc-status", true); | ||
_context2.next = 20; | ||
_context4.next = 20; | ||
break; | ||
case 17: | ||
_context2.prev = 17; | ||
_context2.t1 = _context2["catch"](11); | ||
_context4.prev = 17; | ||
_context4.t1 = _context4["catch"](11); | ||
if (!_context2.t1.response) { | ||
if (!_context4.t1.response) { | ||
this.emit("rpc-status", false); | ||
@@ -208,6 +305,6 @@ } else { | ||
case "end": | ||
return _context2.stop(); | ||
return _context4.stop(); | ||
} | ||
} | ||
}, _callee2, this, [[1, 7], [11, 17]]); | ||
}, _callee4, this, [[1, 7], [11, 17]]); | ||
})); | ||
@@ -260,22 +357,22 @@ | ||
value: function () { | ||
var _query = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3(url) { | ||
var _query = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee5(url) { | ||
var params, | ||
response, | ||
_args3 = arguments; | ||
return regeneratorRuntime.wrap(function _callee3$(_context3) { | ||
_args5 = arguments; | ||
return regeneratorRuntime.wrap(function _callee5$(_context5) { | ||
while (1) { | ||
switch (_context3.prev = _context3.next) { | ||
switch (_context5.prev = _context5.next) { | ||
case 0: | ||
params = _args3.length > 1 && _args3[1] !== undefined ? _args3[1] : ""; | ||
_context3.prev = 1; | ||
_context3.next = 4; | ||
params = _args5.length > 1 && _args5[1] !== undefined ? _args5[1] : ""; | ||
_context5.prev = 1; | ||
_context5.next = 4; | ||
return _axios["default"].get(this.apiAddr + url + params); | ||
case 4: | ||
response = _context3.sent; | ||
return _context3.abrupt("return", response.data); | ||
response = _context5.sent; | ||
return _context5.abrupt("return", response.data); | ||
case 8: | ||
_context3.prev = 8; | ||
_context3.t0 = _context3["catch"](1); | ||
_context5.prev = 8; | ||
_context5.t0 = _context5["catch"](1); | ||
throw "Could not access API: " + this.apiAddr + url + params; | ||
@@ -285,9 +382,9 @@ | ||
case "end": | ||
return _context3.stop(); | ||
return _context5.stop(); | ||
} | ||
} | ||
}, _callee3, this, [[1, 8]]); | ||
}, _callee5, this, [[1, 8]]); | ||
})); | ||
function query(_x2) { | ||
function query(_x4) { | ||
return _query.apply(this, arguments); | ||
@@ -307,3 +404,3 @@ } | ||
value: function toQueryString(rawQuery) { | ||
var _this2 = this; | ||
var _this3 = this; | ||
@@ -315,3 +412,3 @@ var query = rawQuery || {}; | ||
return keys.map(function (key) { | ||
return _typeof(query[key]) === "object" && !Array.isArray(query[key]) ? _this2.toQueryString(query[key]) : _this2.addQueryParam(query, key); | ||
return _typeof(query[key]) === "object" && !Array.isArray(query[key]) ? _this3.toQueryString(query[key]) : _this3.addQueryParam(query, key); | ||
}).join("&"); | ||
@@ -328,11 +425,11 @@ } | ||
value: function () { | ||
var _request = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee4(_ref2) { | ||
var _request = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee6(_ref2) { | ||
var body, path, query, method, url, response, data; | ||
return regeneratorRuntime.wrap(function _callee4$(_context4) { | ||
return regeneratorRuntime.wrap(function _callee6$(_context6) { | ||
while (1) { | ||
switch (_context4.prev = _context4.next) { | ||
switch (_context6.prev = _context6.next) { | ||
case 0: | ||
body = _ref2.body, path = _ref2.path, query = _ref2.query, method = _ref2.method; | ||
url = this.apiAddr + path + this.addQueryParams(query); | ||
_context4.prev = 2; | ||
_context6.prev = 2; | ||
response = (0, _axios["default"])({ | ||
@@ -347,12 +444,12 @@ url: url, | ||
}); | ||
_context4.next = 6; | ||
_context6.next = 6; | ||
return response; | ||
case 6: | ||
data = _context4.sent; | ||
return _context4.abrupt("return", data); | ||
data = _context6.sent; | ||
return _context6.abrupt("return", data); | ||
case 10: | ||
_context4.prev = 10; | ||
_context4.t0 = _context4["catch"](2); | ||
_context6.prev = 10; | ||
_context6.t0 = _context6["catch"](2); | ||
throw "Could not access API: " + url; | ||
@@ -362,9 +459,9 @@ | ||
case "end": | ||
return _context4.stop(); | ||
return _context6.stop(); | ||
} | ||
} | ||
}, _callee4, this, [[2, 10]]); | ||
}, _callee6, this, [[2, 10]]); | ||
})); | ||
function request(_x3) { | ||
function request(_x5) { | ||
return _request.apply(this, arguments); | ||
@@ -371,0 +468,0 @@ } |
{ | ||
"name": "@starport/client-js", | ||
"version": "0.1.12", | ||
"version": "0.1.13", | ||
"description": "A generic JS client for the Websocket, high-level (Cosmos) and low-level (Tendermint) APIs", | ||
@@ -5,0 +5,0 @@ "author": "Tendermint, Inc <hello@tendermint.com>", |
@@ -5,176 +5,213 @@ import { EventEmitter } from "events"; | ||
import { | ||
assertIsBroadcastTxSuccess, | ||
SigningStargateClient | ||
SigningStargateClient, | ||
} from "@cosmjs/stargate"; | ||
import {OfflineDirectSigner} from "@cosmjs/proto-signing" | ||
import { OfflineDirectSigner } from "@cosmjs/proto-signing"; | ||
export interface IClientConfig { | ||
apiAddr: string; | ||
rpcAddr?: string; | ||
wsAddr?: string; | ||
apiAddr: string; | ||
rpcAddr?: string; | ||
wsAddr?: string; | ||
} | ||
export type QueryParamsType = Record<string | number, any>; | ||
export interface IFullRequestParams { | ||
body?: unknown; | ||
path: string; | ||
query?: QueryParamsType; | ||
method: "GET" | "POST" | "PUT"; | ||
body?: unknown; | ||
path: string; | ||
query?: QueryParamsType; | ||
method: "GET" | "POST" | "PUT"; | ||
} | ||
export default class SPClient extends EventEmitter { | ||
private apiAddr: string; | ||
private rpcAddr: string; | ||
private wsAddr: string; | ||
private connectedPromise: Promise<void>; | ||
private socket: ReconnectingWebSocket; | ||
private connectRes; | ||
private connectRej; | ||
public signingClient; | ||
private timer: ReturnType<typeof setInterval>; | ||
private apiAddr: string; | ||
private rpcAddr: string; | ||
private wsAddr: string; | ||
private connectedPromise: Promise<void>; | ||
private socket: ReconnectingWebSocket; | ||
private connectRes; | ||
private connectRej; | ||
public signingClient; | ||
private signer: OfflineDirectSigner; | ||
private timer: ReturnType<typeof setInterval>; | ||
constructor({ apiAddr, rpcAddr, wsAddr }: IClientConfig) { | ||
super(); | ||
this.apiAddr = apiAddr; | ||
this.rpcAddr = rpcAddr; | ||
this.wsAddr = wsAddr; | ||
const poll: any = this.connectivityTest.bind(this); | ||
this.timer = setInterval(poll, 5000); | ||
this.connectivityTest(); | ||
if (this.wsAddr) { | ||
this.connectedPromise = new Promise((res, rej) => { | ||
this.connectRes = res; | ||
this.connectRej = rej; | ||
}); | ||
try { | ||
this.socket = new ReconnectingWebSocket(this.wsAddr); | ||
} catch (e) { | ||
this.connectRej(); | ||
throw "WS node unavailable"; | ||
} | ||
this.socket.onopen = this.onOpenWS.bind(this); | ||
this.socket.onmessage = this.onMessageWS.bind(this); | ||
this.socket.onerror = this.onErrorWS.bind(this); | ||
this.socket.onclose = this.onCloseWS.bind(this); | ||
constructor({ apiAddr, rpcAddr, wsAddr }: IClientConfig) { | ||
super(); | ||
this.apiAddr = apiAddr; | ||
this.rpcAddr = rpcAddr; | ||
this.wsAddr = wsAddr; | ||
const poll: any = this.connectivityTest.bind(this); | ||
this.timer = setInterval(poll, 5000); | ||
this.connectivityTest(); | ||
if (this.wsAddr) { | ||
this.connectedPromise = new Promise((res, rej) => { | ||
this.connectRes = res; | ||
this.connectRej = rej; | ||
}); | ||
try { | ||
this.socket = new ReconnectingWebSocket(this.wsAddr); | ||
} catch (e) { | ||
this.connectRej(); | ||
throw "WS node unavailable"; | ||
} | ||
this.socket.onopen = this.onOpenWS.bind(this); | ||
this.socket.onmessage = this.onMessageWS.bind(this); | ||
this.socket.onerror = this.onErrorWS.bind(this); | ||
this.socket.onclose = this.onCloseWS.bind(this); | ||
} | ||
} | ||
} | ||
public async useSigner(signer:OfflineDirectSigner):Promise<void> { | ||
this.signingClient = await SigningStargateClient.connectWithSigner(this.rpcAddr, signer); | ||
} | ||
private async connectivityTest(): Promise<void> { | ||
if (this.apiAddr) { | ||
try { | ||
await axios.get(this.apiAddr + "/node_info"); | ||
this.emit("api-status", true); | ||
} catch (error) { | ||
if (!error.response) { | ||
this.emit("api-status", false); | ||
} else { | ||
this.emit("api-status", true); | ||
public async useSigner(signer: OfflineDirectSigner): Promise<void> { | ||
this.signingClient = await SigningStargateClient.connectWithSigner( | ||
this.rpcAddr, | ||
signer | ||
); | ||
this.signer = signer; | ||
} | ||
public switchAPI(apiAddr: string):void { | ||
this.apiAddr = apiAddr; | ||
} | ||
public async switchWS(wsAddr: string): Promise<void> { | ||
this.emit("ws-status", false); | ||
this.wsAddr = wsAddr; | ||
this.connectedPromise = new Promise((res, rej) => { | ||
this.connectRes = res; | ||
this.connectRej = rej; | ||
}); | ||
try { | ||
this.socket = new ReconnectingWebSocket(this.wsAddr); | ||
} catch (e) { | ||
this.connectRej(); | ||
throw "WS node unavailable"; | ||
} | ||
} | ||
this.socket.onopen = this.onOpenWS.bind(this); | ||
this.socket.onmessage = this.onMessageWS.bind(this); | ||
this.socket.onerror = this.onErrorWS.bind(this); | ||
this.socket.onclose = this.onCloseWS.bind(this); | ||
return this.connectedPromise; | ||
} | ||
if (this.rpcAddr) { | ||
try { | ||
await axios.get(this.rpcAddr); | ||
this.emit("rpc-status", true); | ||
} catch (error) { | ||
if (!error.response) { | ||
this.emit("rpc-status", false); | ||
} else { | ||
this.emit("api-status", true); | ||
public async switchRPC(rpcAddr: string): Promise<void> { | ||
this.rpcAddr = rpcAddr; | ||
if (this.signingClient) { | ||
const registry = { ...this.signingClient.registry }; | ||
this.signingClient = await SigningStargateClient.connectWithSigner( | ||
this.rpcAddr, | ||
this.signer, | ||
registry | ||
); | ||
} | ||
} | ||
} | ||
} | ||
public connectWS(): Promise<void> { | ||
return this.connectedPromise; | ||
} | ||
private onErrorWS(): void { | ||
this.connectRej(); | ||
} | ||
private onCloseWS(): void { | ||
this.emit("ws-status", false); | ||
} | ||
private onOpenWS(): void { | ||
this.connectRes(); | ||
this.emit("ws-status", true); | ||
this.socket.send( | ||
JSON.stringify({ | ||
jsonrpc: "2.0", | ||
method: "subscribe", | ||
id: "1", | ||
params: ["tm.event = 'NewBlock'"], | ||
}) | ||
); | ||
} | ||
private onMessageWS(msg: any): void { | ||
const result: any = JSON.parse(msg.data).result; | ||
if (result.data && result.data.type === "tendermint/event/NewBlock") { | ||
this.emit("newblock", JSON.parse(msg.data).result); | ||
private async connectivityTest(): Promise<void> { | ||
if (this.apiAddr) { | ||
try { | ||
await axios.get(this.apiAddr + "/node_info"); | ||
this.emit("api-status", true); | ||
} catch (error) { | ||
if (!error.response) { | ||
this.emit("api-status", false); | ||
} else { | ||
this.emit("api-status", true); | ||
} | ||
} | ||
} | ||
if (this.rpcAddr) { | ||
try { | ||
await axios.get(this.rpcAddr); | ||
this.emit("rpc-status", true); | ||
} catch (error) { | ||
if (!error.response) { | ||
this.emit("rpc-status", false); | ||
} else { | ||
this.emit("api-status", true); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
public async query(url: string, params = ""): Promise<any> { | ||
try { | ||
const response: any = await axios.get(this.apiAddr + url + params); | ||
return response.data; | ||
} catch (e) { | ||
throw "Could not access API: " + this.apiAddr + url + params; | ||
public connectWS(): Promise<void> { | ||
return this.connectedPromise; | ||
} | ||
} | ||
private onErrorWS(): void { | ||
this.connectRej(); | ||
} | ||
private onCloseWS(): void { | ||
this.emit("ws-status", false); | ||
} | ||
private onOpenWS(): void { | ||
this.connectRes(); | ||
this.emit("ws-status", true); | ||
this.socket.send( | ||
JSON.stringify({ | ||
jsonrpc: "2.0", | ||
method: "subscribe", | ||
id: "1", | ||
params: ["tm.event = 'NewBlock'"], | ||
}) | ||
); | ||
} | ||
private onMessageWS(msg: any): void { | ||
const result: any = JSON.parse(msg.data).result; | ||
if (result.data && result.data.type === "tendermint/event/NewBlock") { | ||
this.emit("newblock", JSON.parse(msg.data).result); | ||
} | ||
} | ||
public async query(url: string, params = ""): Promise<any> { | ||
try { | ||
const response: any = await axios.get(this.apiAddr + url + params); | ||
return response.data; | ||
} catch (e) { | ||
throw "Could not access API: " + this.apiAddr + url + params; | ||
} | ||
} | ||
private addQueryParam(query: QueryParamsType, key: string): string { | ||
const value: any = query[key]; | ||
private addQueryParam(query: QueryParamsType, key: string): string { | ||
const value: any = query[key]; | ||
return ( | ||
encodeURIComponent(key) + | ||
"=" + | ||
encodeURIComponent( | ||
Array.isArray(value) | ||
? value.join(",") | ||
: typeof value === "number" | ||
? value | ||
: `${value}` | ||
) | ||
); | ||
} | ||
protected toQueryString(rawQuery?: QueryParamsType): string { | ||
const query: QueryParamsType = rawQuery || {}; | ||
const keys: string[] = Object.keys(query).filter( | ||
(key) => "undefined" !== typeof query[key] | ||
); | ||
return keys | ||
.map((key) => | ||
typeof query[key] === "object" && !Array.isArray(query[key]) | ||
? this.toQueryString(query[key] as QueryParamsType) | ||
: this.addQueryParam(query, key) | ||
) | ||
.join("&"); | ||
} | ||
protected addQueryParams(rawQuery?: QueryParamsType): string { | ||
const queryString: string = this.toQueryString(rawQuery); | ||
return queryString ? `?${queryString}` : ""; | ||
} | ||
public async request<T = any>({ | ||
body, | ||
path, | ||
query, | ||
method, | ||
}: IFullRequestParams): Promise<AxiosResponse<T>> { | ||
const url:string =this.apiAddr + path + this.addQueryParams(query); | ||
try { | ||
const response: AxiosPromise<any> = axios({ | ||
url, | ||
return ( | ||
encodeURIComponent(key) + | ||
"=" + | ||
encodeURIComponent( | ||
Array.isArray(value) | ||
? value.join(",") | ||
: typeof value === "number" | ||
? value | ||
: `${value}` | ||
) | ||
); | ||
} | ||
protected toQueryString(rawQuery?: QueryParamsType): string { | ||
const query: QueryParamsType = rawQuery || {}; | ||
const keys: string[] = Object.keys(query).filter( | ||
(key) => "undefined" !== typeof query[key] | ||
); | ||
return keys | ||
.map((key) => | ||
typeof query[key] === "object" && !Array.isArray(query[key]) | ||
? this.toQueryString(query[key] as QueryParamsType) | ||
: this.addQueryParam(query, key) | ||
) | ||
.join("&"); | ||
} | ||
protected addQueryParams(rawQuery?: QueryParamsType): string { | ||
const queryString: string = this.toQueryString(rawQuery); | ||
return queryString ? `?${queryString}` : ""; | ||
} | ||
public async request<T = any>({ | ||
body, | ||
path, | ||
query, | ||
method, | ||
data: body, | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/json;charset=UTF-8", | ||
}, | ||
}); | ||
const data: AxiosResponse<any> = await response; | ||
return data; | ||
} catch (e) { | ||
throw "Could not access API: " + url; | ||
}: IFullRequestParams): Promise<AxiosResponse<T>> { | ||
const url: string = this.apiAddr + path + this.addQueryParams(query); | ||
try { | ||
const response: AxiosPromise<any> = axios({ | ||
url, | ||
method, | ||
data: body, | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/json;charset=UTF-8", | ||
}, | ||
}); | ||
const data: AxiosResponse<any> = await response; | ||
return data; | ||
} catch (e) { | ||
throw "Could not access API: " + url; | ||
} | ||
} | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
38346
603