@tari-project/wallet-daemon-signer 0.5.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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, The Tari Developer Community
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,3 @@
1
+ export { TariConnection } from "./webrtc";
2
+ export * from "@tari-project/tari-permissions";
3
+ export * from "./signer";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { TariConnection } from "./webrtc";
2
+ export * from "@tari-project/tari-permissions";
3
+ export * from "./signer";
@@ -0,0 +1,43 @@
1
+ import { TariPermissions } from "@tari-project/tari-permissions";
2
+ import { SubmitTransactionRequest, TransactionResult, SubmitTransactionResponse, VaultBalances, TemplateDefinition, Substate, ListSubstatesResponse, TariSigner } from "@tari-project/tari-signer";
3
+ import { Account } from "@tari-project/tari-signer";
4
+ import { WalletDaemonClient, SubstateType, ListAccountNftRequest, ListAccountNftResponse } from "@tari-project/wallet_jrpc_client";
5
+ export declare const WalletDaemonNotConnected = "WALLET_DAEMON_NOT_CONNECTED";
6
+ export declare const Unsupported = "UNSUPPORTED";
7
+ export interface WalletDaemonBaseParameters {
8
+ permissions: TariPermissions;
9
+ optionalPermissions?: TariPermissions;
10
+ onConnection?: () => void;
11
+ }
12
+ export interface WalletDaemonParameters extends WalletDaemonBaseParameters {
13
+ signalingServerUrl?: string;
14
+ webRtcConfig?: RTCConfiguration;
15
+ name?: string;
16
+ }
17
+ export interface WalletDaemonFetchParameters extends WalletDaemonBaseParameters {
18
+ serverUrl: string;
19
+ }
20
+ export declare class WalletDaemonTariSigner implements TariSigner {
21
+ signerName: string;
22
+ params: WalletDaemonParameters;
23
+ client: WalletDaemonClient;
24
+ private constructor();
25
+ static build(params: WalletDaemonParameters): Promise<WalletDaemonTariSigner>;
26
+ static buildFetchSigner(params: WalletDaemonFetchParameters): Promise<WalletDaemonTariSigner>;
27
+ private static buildPermissions;
28
+ private getWebRtcTransport;
29
+ get token(): string | undefined;
30
+ get tokenUrl(): string | undefined;
31
+ isConnected(): boolean;
32
+ createFreeTestCoins(): Promise<Account>;
33
+ getAccount(): Promise<Account>;
34
+ getAccountBalances(componentAddress: string): Promise<unknown>;
35
+ getSubstate(substateId: string): Promise<Substate>;
36
+ submitTransaction(req: SubmitTransactionRequest): Promise<SubmitTransactionResponse>;
37
+ getTransactionResult(transactionId: string): Promise<TransactionResult>;
38
+ getPublicKey(branch: string, index: number): Promise<string>;
39
+ getTemplateDefinition(template_address: string): Promise<TemplateDefinition>;
40
+ getConfidentialVaultBalances(viewKeyId: number, vaultId: string, min?: number | null, max?: number | null): Promise<VaultBalances>;
41
+ listSubstates(filter_by_template: string | null, filter_by_type: SubstateType | null, limit: number | null, offset: number | null): Promise<ListSubstatesResponse>;
42
+ getNftsList(req: ListAccountNftRequest): Promise<ListAccountNftResponse>;
43
+ }
package/dist/signer.js ADDED
@@ -0,0 +1,183 @@
1
+ import { TariPermissions } from "@tari-project/tari-permissions";
2
+ import { TariConnection } from "./webrtc";
3
+ import { WalletDaemonClient, substateIdToString, } from "@tari-project/wallet_jrpc_client";
4
+ import { WebRtcRpcTransport } from "./webrtc_transport";
5
+ import { convertStringToTransactionStatus } from "@tari-project/tarijs-types";
6
+ export const WalletDaemonNotConnected = "WALLET_DAEMON_NOT_CONNECTED";
7
+ export const Unsupported = "UNSUPPORTED";
8
+ export class WalletDaemonTariSigner {
9
+ signerName = "WalletDaemon";
10
+ params;
11
+ client;
12
+ constructor(params, connection) {
13
+ this.params = params;
14
+ this.client = connection;
15
+ }
16
+ static async build(params) {
17
+ const allPermissions = WalletDaemonTariSigner.buildPermissions(params);
18
+ let connection = new TariConnection(params.signalingServerUrl, params.webRtcConfig);
19
+ const client = WalletDaemonClient.new(WebRtcRpcTransport.new(connection));
20
+ await connection.init(allPermissions, (conn) => {
21
+ params.onConnection?.();
22
+ if (conn.token) {
23
+ client.setToken(conn.token);
24
+ }
25
+ });
26
+ return new WalletDaemonTariSigner(params, client);
27
+ }
28
+ static async buildFetchSigner(params) {
29
+ const allPermissions = WalletDaemonTariSigner.buildPermissions(params);
30
+ const client = WalletDaemonClient.usingFetchTransport(params.serverUrl);
31
+ const plainPermissions = allPermissions.toJSON().flatMap((p) => (typeof p === "string" ? [p] : []));
32
+ const authResponse = await client.authRequest(plainPermissions);
33
+ await client.authAccept(authResponse, "WalletDaemon");
34
+ params.onConnection?.();
35
+ return new WalletDaemonTariSigner(params, client);
36
+ }
37
+ static buildPermissions(params) {
38
+ const allPermissions = new TariPermissions();
39
+ allPermissions.addPermissions(params.permissions);
40
+ if (params.optionalPermissions) {
41
+ allPermissions.addPermissions(params.optionalPermissions);
42
+ }
43
+ return allPermissions;
44
+ }
45
+ getWebRtcTransport() {
46
+ const transport = this.client.getTransport();
47
+ return transport instanceof WebRtcRpcTransport ? transport : undefined;
48
+ }
49
+ get token() {
50
+ return this.getWebRtcTransport()?.token();
51
+ }
52
+ get tokenUrl() {
53
+ if (!this.token) {
54
+ return undefined;
55
+ }
56
+ const name = (this.params.name && encodeURIComponent(this.params.name)) || "";
57
+ const token = this.token;
58
+ const permissions = JSON.stringify(this.params.permissions);
59
+ const optionalPermissions = JSON.stringify(this.params.optionalPermissions);
60
+ return `tari://${name}/${token}/${permissions}/${optionalPermissions}`;
61
+ }
62
+ isConnected() {
63
+ return this.getWebRtcTransport()?.isConnected() || true;
64
+ }
65
+ async createFreeTestCoins() {
66
+ const res = await this.client.createFreeTestCoins({
67
+ account: { Name: "template_web" },
68
+ amount: 1000000,
69
+ max_fee: null,
70
+ key_id: 0,
71
+ });
72
+ return {
73
+ account_id: res.account.key_index,
74
+ address: res.account.address.Component,
75
+ public_key: res.public_key,
76
+ resources: [],
77
+ };
78
+ }
79
+ async getAccount() {
80
+ const { account, public_key } = (await this.client.accountsGetDefault({}));
81
+ const address = typeof account.address === "object" ? account.address.Component : account.address;
82
+ const { balances } = await this.client.accountsGetBalances({
83
+ account: { ComponentAddress: address },
84
+ refresh: false,
85
+ });
86
+ return {
87
+ account_id: account.key_index,
88
+ address,
89
+ public_key,
90
+ // TODO: should be vaults not resources
91
+ resources: balances.map((b) => ({
92
+ type: b.resource_type,
93
+ resource_address: b.resource_address,
94
+ balance: b.balance + b.confidential_balance,
95
+ vault_id: typeof b.vault_address === "object" && "Vault" in b.vault_address ? b.vault_address.Vault : b.vault_address,
96
+ token_symbol: b.token_symbol,
97
+ })),
98
+ };
99
+ }
100
+ async getAccountBalances(componentAddress) {
101
+ return await this.client.accountsGetBalances({ account: { ComponentAddress: componentAddress }, refresh: true });
102
+ }
103
+ async getSubstate(substateId) {
104
+ // Wallet daemon expects a SubstateId as a string
105
+ const { value, record } = await this.client.substatesGet({ substate_id: substateId });
106
+ return {
107
+ value,
108
+ address: {
109
+ substate_id: substateIdToString(record.substate_id),
110
+ version: record.version,
111
+ },
112
+ };
113
+ }
114
+ async submitTransaction(req) {
115
+ const params = {
116
+ transaction: {
117
+ V1: {
118
+ network: req.network,
119
+ instructions: req.instructions,
120
+ fee_instructions: req.fee_instructions,
121
+ inputs: req.required_substates.map((s) => ({
122
+ // TODO: Hmm The bindings want a SubstateId object, but the wallet only wants a string. Any is used to skip type checking here
123
+ substate_id: s.substate_id,
124
+ version: s.version ?? null,
125
+ })),
126
+ min_epoch: null,
127
+ max_epoch: null,
128
+ is_seal_signer_authorized: req.is_seal_signer_authorized,
129
+ },
130
+ },
131
+ signing_key_index: req.account_id,
132
+ autofill_inputs: [],
133
+ detect_inputs: true,
134
+ proof_ids: [],
135
+ detect_inputs_use_unversioned: req.detect_inputs_use_unversioned,
136
+ };
137
+ const res = await this.client.submitTransaction(params);
138
+ return { transaction_id: res.transaction_id };
139
+ }
140
+ async getTransactionResult(transactionId) {
141
+ const res = await this.client.getTransactionResult({ transaction_id: transactionId });
142
+ return {
143
+ transaction_id: transactionId,
144
+ status: convertStringToTransactionStatus(res.status),
145
+ result: res.result,
146
+ };
147
+ }
148
+ async getPublicKey(branch, index) {
149
+ const res = await this.client.createKey({ branch: branch, specific_index: index });
150
+ return res.public_key;
151
+ }
152
+ async getTemplateDefinition(template_address) {
153
+ let resp = await this.client.templatesGet({ template_address });
154
+ return resp.template_definition;
155
+ }
156
+ async getConfidentialVaultBalances(viewKeyId, vaultId, min = null, max = null) {
157
+ const res = await this.client.viewVaultBalance({
158
+ view_key_id: viewKeyId,
159
+ vault_id: vaultId,
160
+ minimum_expected_value: min,
161
+ maximum_expected_value: max,
162
+ });
163
+ return { balances: res.balances };
164
+ }
165
+ async listSubstates(filter_by_template, filter_by_type, limit, offset) {
166
+ const resp = await this.client.substatesList({
167
+ filter_by_template,
168
+ filter_by_type,
169
+ limit,
170
+ offset,
171
+ });
172
+ const substates = resp.substates.map((s) => ({
173
+ substate_id: typeof s.substate_id === "string" ? s.substate_id : substateIdToString(s.substate_id),
174
+ module_name: s.module_name,
175
+ version: s.version,
176
+ template_address: s.template_address,
177
+ }));
178
+ return { substates };
179
+ }
180
+ async getNftsList(req) {
181
+ return await this.client.nftsList(req);
182
+ }
183
+ }
@@ -0,0 +1,20 @@
1
+ import { TariPermissions } from "@tari-project/tari-permissions";
2
+ import { transports } from "@tari-project/wallet_jrpc_client";
3
+ export declare class TariConnection {
4
+ private _peerConnection;
5
+ private _dataChannel;
6
+ private _signalingServer;
7
+ private _callbacks;
8
+ private _offer?;
9
+ private _walletToken;
10
+ onopen: (() => void) | undefined;
11
+ onConnection: ((conn: TariConnection) => void) | undefined;
12
+ constructor(signalig_server_url?: string, config?: RTCConfiguration);
13
+ get token(): string | undefined;
14
+ init(permissions: TariPermissions, onConnection: ((conn: TariConnection) => void) | undefined): Promise<void>;
15
+ private setAnswer;
16
+ private signalingServerPolling;
17
+ isConnected(): boolean;
18
+ sendMessage<T>(request: transports.RpcRequest, token: string | undefined, timeout_secs?: number | null): Promise<T>;
19
+ private config;
20
+ }
package/dist/webrtc.js ADDED
@@ -0,0 +1,201 @@
1
+ class SignaligServer {
2
+ _token;
3
+ _server_url;
4
+ constructor(server_url) {
5
+ console.log(server_url);
6
+ if (server_url !== undefined) {
7
+ this._server_url = server_url;
8
+ }
9
+ else {
10
+ this._server_url = "http://localhost:9100";
11
+ }
12
+ }
13
+ async initToken(permissions) {
14
+ this._token = await this.authLogin(permissions);
15
+ }
16
+ get token() {
17
+ return this._token;
18
+ }
19
+ async jsonRpc(method, token, params) {
20
+ console.log("jsonRpc", method, token, params);
21
+ let id = 0;
22
+ id += 1;
23
+ let address = this._server_url;
24
+ let headers = { "Content-Type": "application/json" };
25
+ if (token) {
26
+ headers["Authorization"] = `Bearer ${token}`;
27
+ }
28
+ let response = await fetch(address, {
29
+ method: "POST",
30
+ body: JSON.stringify({
31
+ method: method,
32
+ jsonrpc: "2.0",
33
+ id: id,
34
+ params: params || {},
35
+ }),
36
+ headers: headers,
37
+ });
38
+ let json = await response.json();
39
+ if (json.error) {
40
+ throw json.error;
41
+ }
42
+ return json.result;
43
+ }
44
+ async authLogin(permissions) {
45
+ return await this.jsonRpc("auth.login", undefined, permissions);
46
+ }
47
+ async storeIceCandidate(ice_candidate) {
48
+ return await this.jsonRpc("add.offer_ice_candidate", this._token, ice_candidate);
49
+ }
50
+ async storeOffer(offer) {
51
+ return await this.jsonRpc("add.offer", this._token, offer.sdp);
52
+ }
53
+ async getAnswer() {
54
+ return await this.jsonRpc("get.answer", this._token);
55
+ }
56
+ async getIceCandidates() {
57
+ return await this.jsonRpc("get.answer_ice_candidates", this._token);
58
+ }
59
+ }
60
+ export class TariConnection {
61
+ _peerConnection;
62
+ _dataChannel;
63
+ _signalingServer;
64
+ _callbacks;
65
+ _offer;
66
+ _walletToken;
67
+ // This is public so that user can directly set the onopen callback that will be called once the data channel is open.
68
+ onopen;
69
+ onConnection;
70
+ constructor(signalig_server_url, config) {
71
+ this._peerConnection = new RTCPeerConnection(config || this.config());
72
+ this._dataChannel = this._peerConnection.createDataChannel("tari-data");
73
+ this._signalingServer = new SignaligServer(signalig_server_url);
74
+ this._callbacks = {};
75
+ }
76
+ get token() {
77
+ if (this._walletToken) {
78
+ return this._walletToken;
79
+ }
80
+ return this._signalingServer.token;
81
+ }
82
+ async init(permissions, onConnection) {
83
+ this.onConnection = onConnection;
84
+ await this._signalingServer.initToken(permissions);
85
+ // Setup our receiving end
86
+ this._dataChannel.onmessage = (message) => {
87
+ let response = JSON.parse(message.data);
88
+ console.log("response", response);
89
+ if (!this._callbacks[response.id]) {
90
+ console.error("No callback found for id", response.id);
91
+ return;
92
+ }
93
+ // The response should contain id, to identify the Promise.resolve, that is waiting for this result
94
+ let [resolve, reject] = this._callbacks[response.id];
95
+ delete this._callbacks[response.id];
96
+ if (response.payload?.error) {
97
+ reject(new Error(response.payload.error));
98
+ }
99
+ else {
100
+ resolve(response.payload);
101
+ }
102
+ };
103
+ this._dataChannel.onopen = () => {
104
+ // This should be removed before the release, but it's good for debugging.
105
+ console.log("Data channel is open!");
106
+ this.sendMessage({ id: 0, jsonrpc: "2.0", method: "get.token", params: {} }, this._signalingServer.token)
107
+ .then((walletToken) => {
108
+ if (typeof walletToken !== "string") {
109
+ throw Error("Received invalid JWT from wallet daemon");
110
+ }
111
+ console.log("Wallet JWT received: ", walletToken);
112
+ this._walletToken = walletToken;
113
+ if (this.onConnection) {
114
+ this.onConnection(this);
115
+ }
116
+ });
117
+ };
118
+ this._peerConnection.onicecandidate = (event) => {
119
+ console.log("event", event);
120
+ if (event?.candidate) {
121
+ console.log("ICE ", event.candidate);
122
+ console.log("ICE ", typeof event.candidate);
123
+ // Store the ice candidates, so the other end can add them
124
+ this._signalingServer.storeIceCandidate(event.candidate).then((resp) => {
125
+ // This should be removed before the release, but it's good for debugging.
126
+ console.log("Candidate stored", resp);
127
+ });
128
+ }
129
+ };
130
+ // Create offer
131
+ this._offer = await this._peerConnection.createOffer();
132
+ // Set the offer as our local sdp, at this point it will start getting the ice candidates
133
+ this._peerConnection.setLocalDescription(this._offer);
134
+ // Store the offer so the other end can set it as a remote sdp
135
+ this._signalingServer.storeOffer(this._offer).then((resp) => {
136
+ // This should be removed before the release, but it's good for debugging.
137
+ console.log("Offer stored", resp);
138
+ });
139
+ await this.signalingServerPolling();
140
+ }
141
+ async setAnswer() {
142
+ // This is called once the other end got the offer and ices and created and store an answer and its ice candidates
143
+ // We get its answer sdp
144
+ let sdp = await this._signalingServer.getAnswer();
145
+ // And its ice candidates
146
+ let iceCandidates = await this._signalingServer.getIceCandidates();
147
+ // For us the answer is remote sdp
148
+ let answer = new RTCSessionDescription({ sdp, type: "answer" });
149
+ this._peerConnection.setRemoteDescription(answer);
150
+ // We add all the ice candidates to connect, the other end is doing the same with our ice candidates
151
+ for (const iceCandidate of iceCandidates) {
152
+ this._peerConnection.addIceCandidate(iceCandidate);
153
+ }
154
+ }
155
+ async signalingServerPolling() {
156
+ // no need to keep retrying if we are already connected to the wallet
157
+ if (this._peerConnection.connectionState === "connected") {
158
+ return;
159
+ }
160
+ try {
161
+ await this.setAnswer();
162
+ }
163
+ catch (error) {
164
+ // we don't need to do anything on error, as the execution will be retried later
165
+ console.error(error);
166
+ }
167
+ // try again later
168
+ setTimeout(async () => {
169
+ await this.signalingServerPolling();
170
+ }, 2000);
171
+ }
172
+ isConnected() {
173
+ return this._dataChannel.readyState === "open";
174
+ }
175
+ // If the last parameter has timeout property e.g. {timeout:1000}, it set the timeout for this call.
176
+ async sendMessage(request, token, timeout_secs = null) {
177
+ if (!this.isConnected) {
178
+ throw new Error("WALLET_DAEMON_NOT_CONNECTED");
179
+ }
180
+ // This should be removed before the release, but it's good for debugging.
181
+ console.log(request, "timeout", timeout_secs);
182
+ return new Promise((resolve, reject) => {
183
+ // We store the resolve callback for this request,
184
+ // so once the data channel receives a response we know where to return the data
185
+ this._callbacks[request.id] = [resolve, reject];
186
+ if (timeout_secs) {
187
+ // If the user set a timeout which set it here so the promise will be rejected if not fulfilled in time.
188
+ setTimeout(() => {
189
+ delete this._callbacks[request.id];
190
+ reject(new Error("Timeout"));
191
+ }, timeout_secs * 1000);
192
+ }
193
+ // Make the actual call to the wallet daemon
194
+ this._dataChannel.send(JSON.stringify({ token, ...request }));
195
+ });
196
+ }
197
+ // This is our default config, use can set their own stun/turn server in the constructor.
198
+ config() {
199
+ return { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
200
+ }
201
+ }
@@ -0,0 +1,10 @@
1
+ import { transports } from "@tari-project/wallet_jrpc_client";
2
+ import { TariConnection } from "./webrtc";
3
+ export declare class WebRtcRpcTransport implements transports.RpcTransport {
4
+ connection: TariConnection;
5
+ constructor(connection: TariConnection);
6
+ static new(connection: TariConnection): WebRtcRpcTransport;
7
+ token(): string | undefined;
8
+ isConnected(): boolean;
9
+ sendRequest<T>(data: transports.RpcRequest, options: transports.RpcTransportOptions): Promise<T>;
10
+ }
@@ -0,0 +1,18 @@
1
+ export class WebRtcRpcTransport {
2
+ connection;
3
+ constructor(connection) {
4
+ this.connection = connection;
5
+ }
6
+ static new(connection) {
7
+ return new WebRtcRpcTransport(connection);
8
+ }
9
+ token() {
10
+ return this.connection.token;
11
+ }
12
+ isConnected() {
13
+ return this.connection.isConnected();
14
+ }
15
+ sendRequest(data, options) {
16
+ return this.connection.sendMessage(data, options.token);
17
+ }
18
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@tari-project/wallet-daemon-signer",
3
+ "version": "0.5.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "@tari-project/wallet_jrpc_client": "^1.5.1",
14
+ "@tari-project/tari-permissions": "^0.5.0",
15
+ "@tari-project/tari-signer": "^0.5.0",
16
+ "@tari-project/tarijs-types": "^0.5.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.13.1",
20
+ "typescript": "^5.0.4"
21
+ },
22
+ "files": [
23
+ "/dist"
24
+ ],
25
+ "main": "dist/index.js",
26
+ "types": "dist/index.d.ts",
27
+ "scripts": {
28
+ "build": "tsc -b"
29
+ }
30
+ }