@ckb-ccc/joy-id 0.0.5-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.
Files changed (59) hide show
  1. package/README.md +43 -0
  2. package/dist/advanced.d.ts +2 -0
  3. package/dist/advanced.d.ts.map +1 -0
  4. package/dist/advanced.js +1 -0
  5. package/dist/advancedBarrel.d.ts +2 -0
  6. package/dist/advancedBarrel.d.ts.map +1 -0
  7. package/dist/advancedBarrel.js +1 -0
  8. package/dist/barrel.d.ts +4 -0
  9. package/dist/barrel.d.ts.map +1 -0
  10. package/dist/barrel.js +3 -0
  11. package/dist/btc/index.d.ts +17 -0
  12. package/dist/btc/index.d.ts.map +1 -0
  13. package/dist/btc/index.js +73 -0
  14. package/dist/ckb/index.d.ts +25 -0
  15. package/dist/ckb/index.d.ts.map +1 -0
  16. package/dist/ckb/index.js +142 -0
  17. package/dist/connectionsStorage/index.d.ts +23 -0
  18. package/dist/connectionsStorage/index.d.ts.map +1 -0
  19. package/dist/connectionsStorage/index.js +30 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/signerFactory/index.d.ts +3 -0
  24. package/dist/signerFactory/index.d.ts.map +1 -0
  25. package/dist/signerFactory/index.js +27 -0
  26. package/dist.commonjs/advanced.d.ts +2 -0
  27. package/dist.commonjs/advanced.d.ts.map +1 -0
  28. package/dist.commonjs/advanced.js +27 -0
  29. package/dist.commonjs/advancedBarrel.d.ts +2 -0
  30. package/dist.commonjs/advancedBarrel.d.ts.map +1 -0
  31. package/dist.commonjs/advancedBarrel.js +17 -0
  32. package/dist.commonjs/barrel.d.ts +4 -0
  33. package/dist.commonjs/barrel.d.ts.map +1 -0
  34. package/dist.commonjs/barrel.js +19 -0
  35. package/dist.commonjs/btc/index.d.ts +17 -0
  36. package/dist.commonjs/btc/index.d.ts.map +1 -0
  37. package/dist.commonjs/btc/index.js +77 -0
  38. package/dist.commonjs/ckb/index.d.ts +25 -0
  39. package/dist.commonjs/ckb/index.d.ts.map +1 -0
  40. package/dist.commonjs/ckb/index.js +146 -0
  41. package/dist.commonjs/connectionsStorage/index.d.ts +23 -0
  42. package/dist.commonjs/connectionsStorage/index.d.ts.map +1 -0
  43. package/dist.commonjs/connectionsStorage/index.js +35 -0
  44. package/dist.commonjs/index.d.ts +2 -0
  45. package/dist.commonjs/index.d.ts.map +1 -0
  46. package/dist.commonjs/index.js +27 -0
  47. package/dist.commonjs/signerFactory/index.d.ts +3 -0
  48. package/dist.commonjs/signerFactory/index.d.ts.map +1 -0
  49. package/dist.commonjs/signerFactory/index.js +31 -0
  50. package/dist.commonjs/tsconfig.commonjs.tsbuildinfo +1 -0
  51. package/package.json +46 -0
  52. package/src/advanced.ts +1 -0
  53. package/src/advancedBarrel.ts +1 -0
  54. package/src/barrel.ts +3 -0
  55. package/src/btc/index.ts +102 -0
  56. package/src/ckb/index.ts +208 -0
  57. package/src/connectionsStorage/index.ts +57 -0
  58. package/src/index.ts +1 -0
  59. package/src/signerFactory/index.ts +28 -0
@@ -0,0 +1,102 @@
1
+ import { ccc } from "@ckb-ccc/core";
2
+ import { authWithPopup, signMessageWithPopup } from "@joyid/common";
3
+ import {
4
+ Connection,
5
+ ConnectionsRepo,
6
+ ConnectionsRepoLocalStorage,
7
+ } from "../connectionsStorage";
8
+
9
+ export class BitcoinSigner extends ccc.SignerBtc {
10
+ private connection?: Connection;
11
+
12
+ private assertConnection() {
13
+ if (!this.isConnected() || !this.connection) {
14
+ throw new Error("Not connected");
15
+ }
16
+
17
+ return this.connection;
18
+ }
19
+
20
+ constructor(
21
+ client: ccc.Client,
22
+ private readonly addressType: "auto" | "p2wpkh" | "p2tr" = "auto",
23
+ private readonly uri = "https://app.joy.id",
24
+ private readonly connectionsRepo: ConnectionsRepo = new ConnectionsRepoLocalStorage(),
25
+ ) {
26
+ super(client);
27
+ }
28
+
29
+ async replaceClient(client: ccc.Client): Promise<BitcoinSigner> {
30
+ return new BitcoinSigner(client, this.addressType, this.uri);
31
+ }
32
+
33
+ async getBtcAccount(): Promise<string> {
34
+ const { address } = this.assertConnection();
35
+ return address;
36
+ }
37
+
38
+ async getBtcPublicKey(): Promise<ccc.Hex> {
39
+ const { publicKey } = this.assertConnection();
40
+ return publicKey;
41
+ }
42
+
43
+ async connect(): Promise<void> {
44
+ const res = await authWithPopup({
45
+ joyidAppURL: this.uri,
46
+ redirectURL: location.href,
47
+ requestNetwork: `btc-${this.addressType}`,
48
+ });
49
+
50
+ const { address, pubkey } = (() => {
51
+ if (this.addressType === "auto") {
52
+ return res.btcAddressType === "p2wpkh" ? res.nativeSegwit : res.taproot;
53
+ }
54
+ return res.btcAddressType === "p2wpkh" ? res.nativeSegwit : res.taproot;
55
+ })();
56
+
57
+ this.connection = {
58
+ address,
59
+ publicKey: ccc.hexFrom(pubkey),
60
+ keyType: res.keyType,
61
+ };
62
+ await Promise.all([
63
+ this.connectionsRepo.set(
64
+ { uri: this.uri, addressType: `btc-${res.btcAddressType}` },
65
+ this.connection,
66
+ ),
67
+ this.connectionsRepo.set(
68
+ { uri: this.uri, addressType: "btc-auto" },
69
+ this.connection,
70
+ ),
71
+ ]);
72
+ }
73
+
74
+ async isConnected(): Promise<boolean> {
75
+ if (this.connection) {
76
+ return true;
77
+ }
78
+
79
+ this.connection = await this.connectionsRepo.get({
80
+ uri: this.uri,
81
+ addressType: `btc-${this.addressType}`,
82
+ });
83
+ return this.connection !== undefined;
84
+ }
85
+
86
+ async signMessage(message: string | ccc.BytesLike): Promise<string> {
87
+ const { address } = this.assertConnection();
88
+
89
+ const challenge =
90
+ typeof message === "string" ? message : ccc.hexFrom(message).slice(2);
91
+
92
+ const { signature } = await signMessageWithPopup({
93
+ joyidAppURL: this.uri,
94
+ requestNetwork: `btc-${this.addressType}`,
95
+ challenge,
96
+ address,
97
+ signMessageType: "ecdsa",
98
+ redirectURL: location.href,
99
+ });
100
+ return signature;
101
+ }
102
+ }
@@ -0,0 +1,208 @@
1
+ import { ccc } from "@ckb-ccc/core";
2
+ import { Aggregator } from "@joyid/ckb";
3
+ import {
4
+ DappRequestType,
5
+ authWithPopup,
6
+ buildJoyIDURL,
7
+ createBlockDialog,
8
+ openPopup,
9
+ runPopup,
10
+ } from "@joyid/common";
11
+ import {
12
+ Connection,
13
+ ConnectionsRepo,
14
+ ConnectionsRepoLocalStorage,
15
+ } from "../connectionsStorage";
16
+
17
+ export class CkbSigner extends ccc.Signer {
18
+ private connection?: Connection;
19
+
20
+ private async assertConnection(): Promise<Connection> {
21
+ if (!(await this.isConnected()) || !this.connection) {
22
+ throw new Error("Not connected");
23
+ }
24
+
25
+ return this.connection;
26
+ }
27
+
28
+ constructor(
29
+ client: ccc.Client,
30
+ private readonly _uri?: string,
31
+ private readonly _aggregatorUri?: string,
32
+ private readonly connectionsRepo: ConnectionsRepo = new ConnectionsRepoLocalStorage(),
33
+ ) {
34
+ super(client);
35
+ }
36
+
37
+ async replaceClient(client: ccc.Client): Promise<CkbSigner> {
38
+ return new CkbSigner(client, this._uri);
39
+ }
40
+
41
+ private async getUri(): Promise<string> {
42
+ if (this._uri) {
43
+ return this._uri;
44
+ }
45
+
46
+ return (await this.client.getAddressPrefix()) === "ckb"
47
+ ? "https://app.joy.id"
48
+ : "https://testnet.joyid.dev";
49
+ }
50
+
51
+ private async getAggregatorUri(): Promise<string> {
52
+ if (this._aggregatorUri) {
53
+ return this._aggregatorUri;
54
+ }
55
+
56
+ return (await this.client.getAddressPrefix()) === "ckb"
57
+ ? "https://cota.nervina.dev/mainnet-aggregator"
58
+ : "https://cota.nervina.dev/aggregator";
59
+ }
60
+
61
+ async connect(): Promise<void> {
62
+ const uri = await this.getUri();
63
+ const res = await authWithPopup({
64
+ joyidAppURL: uri,
65
+ redirectURL: location.href,
66
+ });
67
+
68
+ this.connection = {
69
+ address: res.address,
70
+ publicKey: ccc.hexFrom(res.pubkey),
71
+ keyType: res.keyType,
72
+ };
73
+ await this.saveConnection();
74
+ }
75
+
76
+ async disconnect(): Promise<void> {
77
+ this.connection = undefined;
78
+ await this.saveConnection();
79
+ }
80
+
81
+ async isConnected(): Promise<boolean> {
82
+ if (this.connection) {
83
+ return true;
84
+ }
85
+ await this.restoreConnection();
86
+ return this.connection !== undefined;
87
+ }
88
+
89
+ async getInternalAddress(): Promise<string> {
90
+ return (await this.assertConnection()).address;
91
+ }
92
+
93
+ async getAddressObj(): Promise<ccc.Address> {
94
+ return await ccc.Address.fromString(
95
+ await this.getInternalAddress(),
96
+ this.client,
97
+ );
98
+ }
99
+
100
+ async getAddressObjs(): Promise<ccc.Address[]> {
101
+ return [await this.getAddressObj()];
102
+ }
103
+
104
+ async prepareTransaction(
105
+ txLike: ccc.TransactionLike,
106
+ ): Promise<ccc.Transaction> {
107
+ const tx = ccc.Transaction.from(txLike);
108
+ const position = await tx.findInputIndexByLock(
109
+ (await this.getAddressObj()).script,
110
+ this.client,
111
+ );
112
+ if (position === undefined) {
113
+ return tx;
114
+ }
115
+
116
+ const witness = tx.getWitnessArgsAt(position) ?? ccc.WitnessArgs.from({});
117
+ witness.lock = "0x";
118
+ await this.prepareTransactionForSubKey(tx, witness);
119
+ return tx.setWitnessArgsAt(position, witness);
120
+ }
121
+
122
+ private async prepareTransactionForSubKey(
123
+ tx: ccc.Transaction,
124
+ witness: ccc.WitnessArgs,
125
+ ) {
126
+ if (this.connection?.keyType !== "sub_key") {
127
+ return [];
128
+ }
129
+
130
+ const pubkeyHash = ccc.ckbHash(this.connection.publicKey).substring(0, 42);
131
+ const lock = (await this.getAddressObj()).script;
132
+ const aggregator = new Aggregator(await this.getAggregatorUri());
133
+ const { unlock_entry: unlockEntry } =
134
+ await aggregator.generateSubkeyUnlockSmt({
135
+ alg_index: 1,
136
+ pubkey_hash: pubkeyHash,
137
+ lock_script: ccc.hexFrom(lock.toBytes()),
138
+ });
139
+ witness.outputType = ccc.hexFrom(unlockEntry);
140
+
141
+ const cotaDeps: ccc.CellDep[] = [];
142
+ for await (const cell of this.client.findCellsByLockAndType(lock, {
143
+ ...(await this.client.getKnownScript(ccc.KnownScript.COTA)),
144
+ args: "0x",
145
+ })) {
146
+ cotaDeps.push(
147
+ ccc.CellDep.from({
148
+ depType: "code",
149
+ outPoint: cell.outPoint,
150
+ }),
151
+ );
152
+ }
153
+
154
+ if (cotaDeps.length === 0) {
155
+ throw new Error("No COTA cells for sub key wallet");
156
+ }
157
+
158
+ tx.cellDeps.unshift(...cotaDeps);
159
+ }
160
+
161
+ async signOnlyTransaction(
162
+ txLike: ccc.TransactionLike,
163
+ ): Promise<ccc.Transaction> {
164
+ const popup = openPopup("");
165
+ if (!popup) {
166
+ return createBlockDialog(async () => this.signOnlyTransaction(txLike));
167
+ }
168
+ const tx = ccc.Transaction.from(txLike);
169
+ const { script } = await this.getAddressObj();
170
+
171
+ popup.location.href = buildJoyIDURL(
172
+ {
173
+ joyidAppURL: await this.getUri(),
174
+ tx: JSON.parse(tx.stringify()),
175
+ signerAddress: (await this.assertConnection()).address,
176
+ redirectURL: location.href,
177
+ witnessIndex: await tx.findInputIndexByLock(script, this.client),
178
+ },
179
+ "popup",
180
+ "/sign-ckb-raw-tx",
181
+ );
182
+
183
+ const res = await runPopup({
184
+ timeoutInSeconds: 3600,
185
+ popup,
186
+ type: DappRequestType.SignCkbRawTx,
187
+ });
188
+
189
+ return ccc.Transaction.from(res.tx);
190
+ }
191
+
192
+ private async saveConnection() {
193
+ return this.connectionsRepo.set(
194
+ {
195
+ uri: await this.getUri(),
196
+ addressType: "ckb",
197
+ },
198
+ this.connection,
199
+ );
200
+ }
201
+
202
+ private async restoreConnection() {
203
+ this.connection = await this.connectionsRepo.get({
204
+ uri: await this.getUri(),
205
+ addressType: "ckb",
206
+ });
207
+ }
208
+ }
@@ -0,0 +1,57 @@
1
+ import { ccc } from "@ckb-ccc/core";
2
+
3
+ export type AccountSelector = {
4
+ uri: string;
5
+ addressType: string;
6
+ };
7
+
8
+ export function isSelectorEq(a: AccountSelector, b: AccountSelector) {
9
+ return a.uri === b.uri && a.addressType === b.addressType;
10
+ }
11
+
12
+ export type Connection = {
13
+ readonly address: string;
14
+ readonly publicKey: ccc.Hex;
15
+ readonly keyType: string;
16
+ };
17
+
18
+ export interface ConnectionsRepo {
19
+ get(selector: AccountSelector): Promise<Connection | undefined>;
20
+ set(
21
+ selector: AccountSelector,
22
+ connection: Connection | undefined,
23
+ ): Promise<void>;
24
+ }
25
+
26
+ export class ConnectionsRepoLocalStorage implements ConnectionsRepo {
27
+ constructor(private readonly storageKey = "ccc-joy-id-signer") {}
28
+
29
+ async readConnections(): Promise<[AccountSelector, Connection][]> {
30
+ return JSON.parse(window.localStorage.getItem(this.storageKey) ?? "[]");
31
+ }
32
+
33
+ async get(selector: AccountSelector): Promise<Connection | undefined> {
34
+ return (await this.readConnections()).find(([s]) =>
35
+ isSelectorEq(selector, s),
36
+ )?.[1];
37
+ }
38
+
39
+ async set(selector: AccountSelector, connection: Connection | undefined) {
40
+ const connections = await this.readConnections();
41
+
42
+ if (connection) {
43
+ const existed = connections.find(([s]) => isSelectorEq(s, selector));
44
+ if (existed) {
45
+ existed[1] = connection;
46
+ } else {
47
+ connections.push([selector, connection]);
48
+ }
49
+ window.localStorage.setItem(this.storageKey, JSON.stringify(connections));
50
+ } else {
51
+ window.localStorage.setItem(
52
+ this.storageKey,
53
+ JSON.stringify(connections.filter(([s]) => !isSelectorEq(s, selector))),
54
+ );
55
+ }
56
+ }
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * as JoyId from "./barrel";
@@ -0,0 +1,28 @@
1
+ import { ccc } from "@ckb-ccc/core";
2
+ import { BitcoinSigner } from "../btc";
3
+ import { CkbSigner } from "../ckb";
4
+
5
+ export function getJoyIdSigners(client: ccc.Client): ccc.SignerInfo[] {
6
+ return [
7
+ {
8
+ type: ccc.SignerType.CKB,
9
+ name: "CKB",
10
+ signer: new CkbSigner(client),
11
+ },
12
+ {
13
+ type: ccc.SignerType.BTC,
14
+ name: "BTC",
15
+ signer: new BitcoinSigner(client),
16
+ },
17
+ {
18
+ type: ccc.SignerType.BTC,
19
+ name: "BTC (P2WPKH)",
20
+ signer: new BitcoinSigner(client, "p2wpkh"),
21
+ },
22
+ {
23
+ type: ccc.SignerType.BTC,
24
+ name: "BTC (P2TR)",
25
+ signer: new BitcoinSigner(client, "p2tr"),
26
+ },
27
+ ];
28
+ }