@ckb-ccc/connector 0.0.9-alpha.2 → 0.0.10-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/chains/index.d.ts +1 -0
- package/dist/assets/chains/index.d.ts.map +1 -1
- package/dist/assets/chains/index.js +1 -0
- package/dist/barrel.d.ts +2 -1
- package/dist/barrel.d.ts.map +1 -1
- package/dist/barrel.js +2 -1
- package/dist/components/button-pill.d.ts +7 -0
- package/dist/components/button-pill.d.ts.map +1 -0
- package/dist/components/button-pill.js +54 -0
- package/dist/components/button.d.ts +7 -0
- package/dist/components/button.d.ts.map +1 -0
- package/dist/components/button.js +60 -0
- package/dist/components/copy-button.d.ts +10 -0
- package/dist/components/copy-button.d.ts.map +1 -0
- package/dist/components/copy-button.js +76 -0
- package/dist/components/dialog.d.ts +8 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +103 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +4 -0
- package/dist/connector/index.d.ts +8 -21
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +59 -496
- package/dist/scenes/connected.d.ts +12 -1
- package/dist/scenes/connected.d.ts.map +1 -1
- package/dist/scenes/connected.js +204 -59
- package/dist/scenes/index.d.ts +2 -3
- package/dist/scenes/index.d.ts.map +1 -1
- package/dist/scenes/index.js +2 -3
- package/dist/scenes/{connecting.d.ts → selecting/connecting.d.ts} +1 -1
- package/dist/scenes/selecting/connecting.d.ts.map +1 -0
- package/dist/scenes/{connecting.js → selecting/connecting.js} +5 -5
- package/dist/scenes/selecting/index.d.ts +19 -0
- package/dist/scenes/selecting/index.d.ts.map +1 -0
- package/dist/scenes/selecting/index.js +164 -0
- package/dist/scenes/{signers.d.ts → selecting/signers.d.ts} +1 -1
- package/dist/scenes/selecting/signers.d.ts.map +1 -0
- package/dist/scenes/{signers.js → selecting/signers.js} +6 -7
- package/dist/scenes/{wallets.d.ts → selecting/wallets.d.ts} +1 -1
- package/dist/scenes/selecting/wallets.d.ts.map +1 -0
- package/dist/scenes/{wallets.js → selecting/wallets.js} +3 -3
- package/dist/signers/index.d.ts +28 -0
- package/dist/signers/index.d.ts.map +1 -0
- package/dist/signers/index.js +104 -0
- package/package.json +3 -3
- package/src/assets/chains/index.ts +1 -0
- package/src/barrel.ts +3 -1
- package/src/components/button-pill.ts +48 -0
- package/src/components/button.ts +54 -0
- package/src/components/copy-button.ts +69 -0
- package/src/components/dialog.ts +97 -0
- package/src/components/index.ts +4 -0
- package/src/connector/index.ts +67 -596
- package/src/scenes/connected.ts +197 -68
- package/src/scenes/index.ts +2 -3
- package/src/scenes/{connecting.ts → selecting/connecting.ts} +6 -6
- package/src/scenes/selecting/index.ts +181 -0
- package/src/scenes/{signers.ts → selecting/signers.ts} +7 -8
- package/src/scenes/{wallets.ts → selecting/wallets.ts} +4 -4
- package/src/signers/index.ts +186 -0
- package/dist/assets/chains/ckb.sm.svg.d.ts +0 -2
- package/dist/assets/chains/ckb.sm.svg.d.ts.map +0 -1
- package/dist/assets/chains/ckb.sm.svg.js +0 -2
- package/dist/assets/copy.svg.d.ts +0 -2
- package/dist/assets/copy.svg.d.ts.map +0 -1
- package/dist/assets/copy.svg.js +0 -2
- package/dist/events/index.d.ts +0 -7
- package/dist/events/index.d.ts.map +0 -1
- package/dist/events/index.js +0 -10
- package/dist/scenes/connecting.d.ts.map +0 -1
- package/dist/scenes/signers.d.ts.map +0 -1
- package/dist/scenes/wallets.d.ts.map +0 -1
- package/src/assets/chains/ckb.sm.svg.ts +0 -5
- package/src/assets/copy.svg.ts +0 -5
- package/src/events/index.ts +0 -11
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ccc } from "@ckb-ccc/ccc";
|
|
2
|
+
import { NOSTR_SVG } from "../assets/chains/nostr.svg";
|
|
3
|
+
import { JOY_ID_SVG } from "../assets/joy-id.svg";
|
|
4
|
+
import { METAMASK_SVG } from "../assets/metamask.svg";
|
|
5
|
+
import { OKX_SVG } from "../assets/okx.svg";
|
|
6
|
+
import { UNI_SAT_SVG } from "../assets/uni-sat.svg";
|
|
7
|
+
export class SignersController {
|
|
8
|
+
constructor(host) {
|
|
9
|
+
this.host = host;
|
|
10
|
+
this.wallets = [];
|
|
11
|
+
this.resetListeners = [];
|
|
12
|
+
host.addController(this);
|
|
13
|
+
}
|
|
14
|
+
getConfig() {
|
|
15
|
+
const name = this.host.name ??
|
|
16
|
+
document.querySelector("head title").text;
|
|
17
|
+
const icon = this.host.icon ??
|
|
18
|
+
document.querySelector('link[rel="icon"]').href;
|
|
19
|
+
const preferredNetworks = [
|
|
20
|
+
...(this.host.preferredNetworks ?? []),
|
|
21
|
+
{
|
|
22
|
+
addressPrefix: "ckb",
|
|
23
|
+
signerType: ccc.SignerType.BTC,
|
|
24
|
+
network: "btc",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
addressPrefix: "ckt",
|
|
28
|
+
signerType: ccc.SignerType.BTC,
|
|
29
|
+
network: "btcTestnet",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
return {
|
|
33
|
+
client: this.host.client,
|
|
34
|
+
name,
|
|
35
|
+
icon,
|
|
36
|
+
preferredNetworks,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
update() {
|
|
40
|
+
this.host.refreshSigner();
|
|
41
|
+
this.host.requestUpdate();
|
|
42
|
+
}
|
|
43
|
+
refresh() {
|
|
44
|
+
this.resetListeners.forEach((listener) => listener());
|
|
45
|
+
this.resetListeners = [];
|
|
46
|
+
this.wallets = [];
|
|
47
|
+
const { client, name, icon, preferredNetworks } = this.getConfig();
|
|
48
|
+
ccc.JoyId.getJoyIdSigners(client, name, icon, preferredNetworks).forEach(({ signer, name }) => {
|
|
49
|
+
this.addSigner("JoyID", name, JOY_ID_SVG, signer);
|
|
50
|
+
});
|
|
51
|
+
const uniSatSigner = ccc.UniSat.getUniSatSigner(client, preferredNetworks);
|
|
52
|
+
if (uniSatSigner) {
|
|
53
|
+
this.addSigner("UniSat", "BTC", UNI_SAT_SVG, uniSatSigner);
|
|
54
|
+
}
|
|
55
|
+
ccc.Okx.getOKXBitcoinSigner(client, preferredNetworks).forEach((signer) => {
|
|
56
|
+
this.addSigner("OKX Wallet", signer.type, OKX_SVG, signer);
|
|
57
|
+
});
|
|
58
|
+
const nip07Signer = ccc.Nip07.getNip07Signer(client);
|
|
59
|
+
if (nip07Signer) {
|
|
60
|
+
this.addSigner("Nostr", "Nostr", NOSTR_SVG, nip07Signer);
|
|
61
|
+
}
|
|
62
|
+
const eip6963Manager = new ccc.Eip6963.SignerFactory(client);
|
|
63
|
+
this.resetListeners.push(eip6963Manager.subscribeSigners((signer) => {
|
|
64
|
+
this.addSigner(`${signer.detail.info.name}`, "EVM", signer.detail.info.icon, signer);
|
|
65
|
+
this.update();
|
|
66
|
+
}));
|
|
67
|
+
// === Dummy signers ===
|
|
68
|
+
this.addSigner("MetaMask", "EVM", METAMASK_SVG, new ccc.SignerOpenLink(client, ccc.SignerType.EVM, `https://metamask.app.link/dapp/${window.location.href}`));
|
|
69
|
+
[ccc.SignerType.EVM, ccc.SignerType.BTC].forEach((type) => {
|
|
70
|
+
this.addSigner("OKX Wallet", type, OKX_SVG, new ccc.SignerOpenLink(client, type, "https://www.okx.com/download?deeplink=" +
|
|
71
|
+
encodeURIComponent("okx://wallet/dapp/url?dappUrl=" +
|
|
72
|
+
encodeURIComponent(window.location.href))));
|
|
73
|
+
});
|
|
74
|
+
this.addSigner("UniSat", ccc.SignerType.BTC, UNI_SAT_SVG, new ccc.SignerOpenLink(client, ccc.SignerType.BTC, "https://unisat.io/download"));
|
|
75
|
+
// ===
|
|
76
|
+
}
|
|
77
|
+
async addSigner(walletName, signerName, icon, signer) {
|
|
78
|
+
const signerInfo = { name: signerName, signer };
|
|
79
|
+
const signerFilter = this.host.signerFilter;
|
|
80
|
+
if (signerFilter &&
|
|
81
|
+
!(await signerFilter(signerInfo, { name: walletName, icon }))) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const wallet = this.wallets.find((w) => w.name === walletName);
|
|
85
|
+
if (!wallet) {
|
|
86
|
+
this.wallets.push({
|
|
87
|
+
name: walletName,
|
|
88
|
+
icon,
|
|
89
|
+
signers: [signerInfo],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const signers = [...wallet.signers, signerInfo].filter(({ signer }) => !(signer instanceof ccc.SignerDummy));
|
|
94
|
+
wallet.signers = signers.length !== 0 ? signers : [signerInfo];
|
|
95
|
+
}
|
|
96
|
+
this.update();
|
|
97
|
+
}
|
|
98
|
+
hostConnected() {
|
|
99
|
+
this.refresh();
|
|
100
|
+
// Wait for plugins to be loaded
|
|
101
|
+
setTimeout(() => this.refresh(), 100);
|
|
102
|
+
}
|
|
103
|
+
hostDisconnected() { }
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ckb-ccc/connector",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10-alpha.0",
|
|
4
4
|
"description": "CCC - CKBer's Codebase. Common Chains Connector UI",
|
|
5
5
|
"author": "Hanssen0 <hanssen0@hanssen0.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@ckb-ccc/ccc": "0.0.
|
|
40
|
+
"@ckb-ccc/ccc": "0.0.10-alpha.0",
|
|
41
41
|
"lit": "^3.1.3"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "8a4c789419f5e12e1f230cead213c59373834a1a"
|
|
44
44
|
}
|
package/src/barrel.ts
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { css, html, LitElement } from "lit";
|
|
2
|
+
import { customElement } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
@customElement("ccc-button-pill")
|
|
5
|
+
export class ButtonPill extends LitElement {
|
|
6
|
+
static styles = css`
|
|
7
|
+
button {
|
|
8
|
+
background: none;
|
|
9
|
+
color: inherit;
|
|
10
|
+
border: none;
|
|
11
|
+
padding: 0;
|
|
12
|
+
font: inherit;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
outline: inherit;
|
|
15
|
+
|
|
16
|
+
width: 100%;
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
padding: 0.25rem 1rem;
|
|
20
|
+
background: var(--btn-secondary);
|
|
21
|
+
border-radius: 9999px;
|
|
22
|
+
transition: background 0.15s ease-in-out;
|
|
23
|
+
}
|
|
24
|
+
button:hover {
|
|
25
|
+
background: var(--btn-secondary-hover);
|
|
26
|
+
}
|
|
27
|
+
button:disabled {
|
|
28
|
+
cursor: not-allowed;
|
|
29
|
+
}
|
|
30
|
+
button:disabled:hover {
|
|
31
|
+
background: var(--btn-secondary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
button ::slotted(img) {
|
|
35
|
+
width: 0.8rem;
|
|
36
|
+
height: 0.8rem;
|
|
37
|
+
margin-right: 0.5rem;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
updated() {
|
|
42
|
+
this.dispatchEvent(new Event("updated", { bubbles: true, composed: true }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
render() {
|
|
46
|
+
return html`<button><slot></slot></button>`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { css, html, LitElement } from "lit";
|
|
2
|
+
import { customElement } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
@customElement("ccc-button")
|
|
5
|
+
export class Button extends LitElement {
|
|
6
|
+
static styles = css`
|
|
7
|
+
:host {
|
|
8
|
+
width: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
button {
|
|
12
|
+
background: none;
|
|
13
|
+
color: inherit;
|
|
14
|
+
border: none;
|
|
15
|
+
padding: 0;
|
|
16
|
+
font: inherit;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
outline: inherit;
|
|
19
|
+
|
|
20
|
+
width: 100%;
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: start;
|
|
24
|
+
padding: 0.75rem 1rem;
|
|
25
|
+
background: var(--btn-primary);
|
|
26
|
+
border-radius: 0.4rem;
|
|
27
|
+
transition: background 0.15s ease-in-out;
|
|
28
|
+
}
|
|
29
|
+
button:hover {
|
|
30
|
+
background: var(--btn-primary-hover);
|
|
31
|
+
}
|
|
32
|
+
button:disabled {
|
|
33
|
+
cursor: not-allowed;
|
|
34
|
+
}
|
|
35
|
+
button:disabled:hover {
|
|
36
|
+
background: var(--btn-primary);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
button ::slotted(img) {
|
|
40
|
+
width: 2rem;
|
|
41
|
+
height: 2rem;
|
|
42
|
+
margin-right: 1rem;
|
|
43
|
+
border-radius: 0.4rem;
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
updated() {
|
|
48
|
+
this.dispatchEvent(new Event("updated", { bubbles: true, composed: true }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render() {
|
|
52
|
+
return html`<button><slot></slot></button>`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { css, html, LitElement } from "lit";
|
|
2
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
@customElement("ccc-copy-button")
|
|
5
|
+
export class CopyButton extends LitElement {
|
|
6
|
+
@property()
|
|
7
|
+
public value?: string;
|
|
8
|
+
|
|
9
|
+
@state()
|
|
10
|
+
private isCopied = false;
|
|
11
|
+
|
|
12
|
+
static styles = css`
|
|
13
|
+
:host {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
}
|
|
18
|
+
.copy {
|
|
19
|
+
width: 1em;
|
|
20
|
+
height: 1em;
|
|
21
|
+
fill: currentColor;
|
|
22
|
+
}
|
|
23
|
+
.check {
|
|
24
|
+
width: 0.7em;
|
|
25
|
+
height: 0.7em;
|
|
26
|
+
margin-left: 0.15em;
|
|
27
|
+
fill: currentColor;
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
updated() {
|
|
32
|
+
this.dispatchEvent(new Event("updated", { bubbles: true, composed: true }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onclick = () => {
|
|
36
|
+
if (!this.value) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.isCopied = true;
|
|
41
|
+
setTimeout(() => (this.isCopied = false), 3000);
|
|
42
|
+
|
|
43
|
+
return window.navigator.clipboard.writeText(this.value);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
render() {
|
|
47
|
+
return html`
|
|
48
|
+
<slot></slot>
|
|
49
|
+
${this.isCopied
|
|
50
|
+
? html`
|
|
51
|
+
<svg class="check" viewBox="0 0 17.837 17.837" alt="copied">
|
|
52
|
+
<path
|
|
53
|
+
d="M16.145,2.571c-0.272-0.273-0.718-0.273-0.99,0L6.92,10.804l-4.241-4.27c-0.272-0.274-0.715-0.274-0.989,0L0.204,8.019c-0.272,0.271-0.272,0.717,0,0.99l6.217,6.258c0.272,0.271,0.715,0.271,0.99,0L17.63,5.047c0.276-0.273,0.276-0.72,0-0.994L16.145,2.571z"
|
|
54
|
+
/>
|
|
55
|
+
</svg>
|
|
56
|
+
`
|
|
57
|
+
: html`
|
|
58
|
+
<svg class="copy" viewBox="0 0 24 24" alt="copy">
|
|
59
|
+
<path
|
|
60
|
+
d="M19 13.5657V6.10526C19 5.49737 18.5091 5 17.9091 5H10.5454C9.94543 5 9.45452 5.49737 9.45452 6.10526V13.5657C9.45452 14.1736 9.94543 14.671 10.5454 14.671H17.9091C18.5091 14.671 19 14.1736 19 13.5657Z"
|
|
61
|
+
/>
|
|
62
|
+
<path
|
|
63
|
+
d="M14.5455 15.8684H9.18183C8.58183 15.8684 8.09092 15.3711 8.09092 14.7632V9.329H6.09091C5.49091 9.329 5 9.82637 5 10.4343V17.8947C5 18.5026 5.49091 19 6.09091 19H13.4546C14.0546 19 14.5455 18.5026 14.5455 17.8947V15.8684Z"
|
|
64
|
+
/>
|
|
65
|
+
</svg>
|
|
66
|
+
`}
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { css, html, LitElement } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import { CLOSE_SVG } from "../assets/close.svg";
|
|
4
|
+
import { LEFT_SVG } from "../assets/left.svg";
|
|
5
|
+
|
|
6
|
+
@customElement("ccc-dialog")
|
|
7
|
+
export class Dialog extends LitElement {
|
|
8
|
+
@property()
|
|
9
|
+
public canBack: any | undefined;
|
|
10
|
+
@property()
|
|
11
|
+
public header?: string;
|
|
12
|
+
|
|
13
|
+
render() {
|
|
14
|
+
return html`
|
|
15
|
+
<div
|
|
16
|
+
class="header text-bold fs-lg ${this.header ? "header-divider" : ""}"
|
|
17
|
+
>
|
|
18
|
+
<img
|
|
19
|
+
class="back ${this.canBack !== undefined ? "active" : ""}"
|
|
20
|
+
src=${LEFT_SVG}
|
|
21
|
+
@click=${() => {
|
|
22
|
+
this.dispatchEvent(new Event("back"));
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
${this.header}
|
|
26
|
+
<img
|
|
27
|
+
class="close active"
|
|
28
|
+
src=${CLOSE_SVG}
|
|
29
|
+
@click=${() => {
|
|
30
|
+
this.dispatchEvent(
|
|
31
|
+
new Event("close", { bubbles: true, composed: true }),
|
|
32
|
+
);
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="body">
|
|
37
|
+
<slot></slot>
|
|
38
|
+
</div>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static styles = css`
|
|
43
|
+
.text-bold {
|
|
44
|
+
font-weight: bold;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.fs-lg {
|
|
48
|
+
font-size: 1.2rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.main {
|
|
52
|
+
position: fixed;
|
|
53
|
+
left: 50%;
|
|
54
|
+
top: 50%;
|
|
55
|
+
transform: translate(-50%, -50%);
|
|
56
|
+
background: var(--background);
|
|
57
|
+
border-radius: 1.2rem;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
transition: height 0.15s ease-out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.header {
|
|
63
|
+
display: flex;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
align-items: center;
|
|
66
|
+
padding: 1rem 1.3rem 0.5rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.header-divider {
|
|
70
|
+
padding-bottom: 1rem;
|
|
71
|
+
border-bottom: 1px solid var(--divider);
|
|
72
|
+
margin-bottom: 0.3rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.close,
|
|
76
|
+
.back {
|
|
77
|
+
width: 0.8rem;
|
|
78
|
+
height: 0.8rem;
|
|
79
|
+
opacity: 0;
|
|
80
|
+
transition: opacity 0.15s ease-in-out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.close.active,
|
|
84
|
+
.back.active {
|
|
85
|
+
opacity: 1;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.body {
|
|
90
|
+
padding: 0 1.3rem 1rem;
|
|
91
|
+
min-width: 20rem;
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
align-items: center;
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
}
|