@idos-network/kwil-infra 1.1.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/.turbo/turbo-build.log +47 -0
- package/.turbo/turbo-test.log +126 -0
- package/.turbo/turbo-typecheck.log +5 -0
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/actions/index.d.mts +2 -0
- package/dist/actions/index.mjs +1371 -0
- package/dist/actions/index.mjs.map +1 -0
- package/dist/facesign/index.d.mts +2 -0
- package/dist/facesign/index.mjs +2 -0
- package/dist/facesign-signer-ADd4EASX.d.mts +29 -0
- package/dist/facesign-signer-ADd4EASX.d.mts.map +1 -0
- package/dist/facesign-signer-BY9Nti9F.mjs +244 -0
- package/dist/facesign-signer-BY9Nti9F.mjs.map +1 -0
- package/dist/index-Ci6QhKJo.d.mts +644 -0
- package/dist/index-Ci6QhKJo.d.mts.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +429 -0
- package/dist/index.mjs.map +1 -0
- package/dist/signature-verification/index.d.mts +14 -0
- package/dist/signature-verification/index.d.mts.map +1 -0
- package/dist/signature-verification/index.mjs +140 -0
- package/dist/signature-verification/index.mjs.map +1 -0
- package/dist/xrp/utils.d.mts +1165 -0
- package/dist/xrp/utils.d.mts.map +1 -0
- package/dist/xrp/utils.mjs +105 -0
- package/dist/xrp/utils.mjs.map +1 -0
- package/dist/xrpl-credentials/index.d.mts +179 -0
- package/dist/xrpl-credentials/index.d.mts.map +1 -0
- package/dist/xrpl-credentials/index.mjs +224 -0
- package/dist/xrpl-credentials/index.mjs.map +1 -0
- package/package.json +93 -0
- package/src/actions/actions.ts +2197 -0
- package/src/actions/index.ts +55 -0
- package/src/create-kwil-client.ts +144 -0
- package/src/create-kwil-signer.ts +270 -0
- package/src/facesign/facesign-signer.ts +327 -0
- package/src/facesign/index.ts +2 -0
- package/src/index.ts +7 -0
- package/src/near/create-near-wallet-kwil-signer.ts +245 -0
- package/src/near/get-config.ts +43 -0
- package/src/near/nep413.ts +62 -0
- package/src/near/signature-verification.ts +85 -0
- package/src/signature-verification/evm.ts +33 -0
- package/src/signature-verification/facesign.ts +19 -0
- package/src/signature-verification/index.test.ts +110 -0
- package/src/signature-verification/index.ts +72 -0
- package/src/signature-verification/stellar.ts +31 -0
- package/src/xrp/signature-verification.ts +18 -0
- package/src/xrp/signer.ts +43 -0
- package/src/xrp/utils.ts +178 -0
- package/src/xrpl-credentials/accept.ts +143 -0
- package/src/xrpl-credentials/create.ts +199 -0
- package/src/xrpl-credentials/index.ts +12 -0
- package/tsconfig.json +33 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +6 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// Buffer after showing the iframe before posting a message, giving the enclave time to be ready.
|
|
2
|
+
const POST_MESSAGE_DELAY_MS = 300;
|
|
3
|
+
// Safety net: reject proposals if the enclave doesn't respond within this window.
|
|
4
|
+
const PROPOSAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
5
|
+
|
|
6
|
+
export interface FaceSignMetadata {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FaceSignSignerProviderOptions {
|
|
12
|
+
metadata: FaceSignMetadata;
|
|
13
|
+
enclaveUrl: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SessionResponse {
|
|
17
|
+
address: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class FaceSignSignerProvider {
|
|
21
|
+
#iframe: HTMLIFrameElement | null = null;
|
|
22
|
+
#container: HTMLDialogElement | null = null;
|
|
23
|
+
|
|
24
|
+
#resolveOpenEnclave: (() => void) | null = null;
|
|
25
|
+
#resolveSignMessage: ((signature: Uint8Array) => void) | null = null;
|
|
26
|
+
#rejectSignMessage: ((error: Error) => void) | null = null;
|
|
27
|
+
#resolveSessionProposal: ((response: SessionResponse) => void) | null = null;
|
|
28
|
+
#rejectSessionProposal: ((error: Error) => void) | null = null;
|
|
29
|
+
#resolveAddressRequest: ((address: string | null) => void) | null = null;
|
|
30
|
+
|
|
31
|
+
#messageListener: ((event: MessageEvent) => void) | null = null;
|
|
32
|
+
#messageListenerInitialized = false;
|
|
33
|
+
#proposalId = 0;
|
|
34
|
+
#hasKey: boolean | null = null;
|
|
35
|
+
#metadata: FaceSignMetadata;
|
|
36
|
+
#enclaveUrl: string;
|
|
37
|
+
#enclaveOrigin: string;
|
|
38
|
+
|
|
39
|
+
signatureType = "ed25519";
|
|
40
|
+
walletType = "FaceSign";
|
|
41
|
+
publicAddress = "";
|
|
42
|
+
publicKey = "";
|
|
43
|
+
|
|
44
|
+
constructor(options: FaceSignSignerProviderOptions) {
|
|
45
|
+
this.#metadata = options.metadata;
|
|
46
|
+
this.#enclaveUrl = options.enclaveUrl;
|
|
47
|
+
this.#enclaveOrigin = new URL(options.enclaveUrl).origin;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#setupMessageListener(): void {
|
|
51
|
+
if (this.#messageListenerInitialized) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.#messageListener = (event: MessageEvent) => {
|
|
56
|
+
if (event.origin !== this.#enclaveOrigin) return;
|
|
57
|
+
|
|
58
|
+
if (event.data?.type === "facesign_ready") {
|
|
59
|
+
this.#hasKey = !!event.data.hasKey;
|
|
60
|
+
this.#resolveOpenEnclave?.();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (event.data?.type === "sign_proposal_response") {
|
|
64
|
+
const payload = event.data.data;
|
|
65
|
+
if (!payload) return;
|
|
66
|
+
|
|
67
|
+
if (payload.signature) {
|
|
68
|
+
this.#resolveSignMessage?.(new Uint8Array(payload.signature));
|
|
69
|
+
} else {
|
|
70
|
+
this.#rejectSignMessage?.(new Error("Sign request rejected by user"));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (event.data?.type === "session_proposal_response") {
|
|
75
|
+
const payload = event.data.data;
|
|
76
|
+
if (!payload) return;
|
|
77
|
+
|
|
78
|
+
if (payload.approved && payload.address) {
|
|
79
|
+
this.#resolveSessionProposal?.({ address: payload.address });
|
|
80
|
+
} else {
|
|
81
|
+
this.#rejectSessionProposal?.(new Error("Session request rejected by user"));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (event.data?.type === "address_response") {
|
|
86
|
+
const payload = event.data.data;
|
|
87
|
+
this.#resolveAddressRequest?.(payload?.address ?? null);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
window.addEventListener("message", this.#messageListener);
|
|
92
|
+
this.#messageListenerInitialized = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async preload(): Promise<{ hasKey: boolean }> {
|
|
96
|
+
this.#setupMessageListener();
|
|
97
|
+
await this.#ensureEnclave();
|
|
98
|
+
return { hasKey: this.#hasKey ?? false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async init(): Promise<string> {
|
|
102
|
+
this.#setupMessageListener();
|
|
103
|
+
|
|
104
|
+
const sessionProposal = await this.#sessionProposal();
|
|
105
|
+
|
|
106
|
+
this.publicKey = sessionProposal.address;
|
|
107
|
+
this.publicAddress = sessionProposal.address;
|
|
108
|
+
|
|
109
|
+
return sessionProposal.address;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getAddress(): Promise<string | null> {
|
|
113
|
+
this.#setupMessageListener();
|
|
114
|
+
await this.#ensureEnclave();
|
|
115
|
+
|
|
116
|
+
if (!this.#hasKey) return null;
|
|
117
|
+
|
|
118
|
+
return new Promise<string | null>((resolve) => {
|
|
119
|
+
const timer = setTimeout(() => resolve(null), PROPOSAL_TIMEOUT_MS);
|
|
120
|
+
|
|
121
|
+
this.#resolveAddressRequest = (address) => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
this.#resolveAddressRequest = null;
|
|
124
|
+
if (address) {
|
|
125
|
+
this.publicKey = address;
|
|
126
|
+
this.publicAddress = address;
|
|
127
|
+
}
|
|
128
|
+
resolve(address);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
this.#iframe?.contentWindow?.postMessage(
|
|
132
|
+
{ type: "address_request", data: { id: ++this.#proposalId } },
|
|
133
|
+
this.#enclaveUrl,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get signer(): this {
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async signMessage(message: Uint8Array | string): Promise<Uint8Array> {
|
|
143
|
+
await this.#ensureEnclave();
|
|
144
|
+
this.#showEnclave();
|
|
145
|
+
|
|
146
|
+
const messageToSign = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
return await new Promise<Uint8Array>((resolve, reject) => {
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
reject(new Error("FaceSign sign request timed out"));
|
|
152
|
+
}, PROPOSAL_TIMEOUT_MS);
|
|
153
|
+
|
|
154
|
+
this.#resolveSignMessage = (sig) => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
resolve(sig);
|
|
157
|
+
};
|
|
158
|
+
this.#rejectSignMessage = (err) => {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
reject(err);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
this.#iframe?.contentWindow?.postMessage(
|
|
165
|
+
{
|
|
166
|
+
type: "sign_proposal",
|
|
167
|
+
data: {
|
|
168
|
+
id: ++this.#proposalId,
|
|
169
|
+
data: messageToSign,
|
|
170
|
+
metadata: this.#metadata,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
this.#enclaveUrl,
|
|
174
|
+
);
|
|
175
|
+
}, POST_MESSAGE_DELAY_MS);
|
|
176
|
+
});
|
|
177
|
+
} finally {
|
|
178
|
+
this.#resolveSignMessage = null;
|
|
179
|
+
this.#rejectSignMessage = null;
|
|
180
|
+
this.#hideEnclave();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async #sessionProposal(): Promise<SessionResponse> {
|
|
185
|
+
await this.#ensureEnclave();
|
|
186
|
+
this.#showEnclave();
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
return await new Promise<SessionResponse>((resolve, reject) => {
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
reject(new Error("FaceSign session proposal timed out"));
|
|
192
|
+
}, PROPOSAL_TIMEOUT_MS);
|
|
193
|
+
|
|
194
|
+
this.#resolveSessionProposal = (res) => {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
resolve(res);
|
|
197
|
+
};
|
|
198
|
+
this.#rejectSessionProposal = (err) => {
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
reject(err);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
this.#iframe?.contentWindow?.postMessage(
|
|
205
|
+
{
|
|
206
|
+
type: "session_proposal",
|
|
207
|
+
data: {
|
|
208
|
+
id: ++this.#proposalId,
|
|
209
|
+
metadata: this.#metadata,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
this.#enclaveUrl,
|
|
213
|
+
);
|
|
214
|
+
}, POST_MESSAGE_DELAY_MS);
|
|
215
|
+
});
|
|
216
|
+
} finally {
|
|
217
|
+
this.#resolveSessionProposal = null;
|
|
218
|
+
this.#rejectSessionProposal = null;
|
|
219
|
+
this.#hideEnclave();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
hide(): void {
|
|
224
|
+
this.#cancelPendingProposals();
|
|
225
|
+
this.#hideEnclave();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#cancelPendingProposals(): void {
|
|
229
|
+
const cancelError = new Error("FaceSign operation cancelled");
|
|
230
|
+
|
|
231
|
+
if (this.#rejectSignMessage) {
|
|
232
|
+
this.#rejectSignMessage(cancelError);
|
|
233
|
+
this.#resolveSignMessage = null;
|
|
234
|
+
this.#rejectSignMessage = null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (this.#rejectSessionProposal) {
|
|
238
|
+
this.#rejectSessionProposal(cancelError);
|
|
239
|
+
this.#resolveSessionProposal = null;
|
|
240
|
+
this.#rejectSessionProposal = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
destroy(): void {
|
|
245
|
+
this.#cancelPendingProposals();
|
|
246
|
+
if (this.#messageListener) {
|
|
247
|
+
window.removeEventListener("message", this.#messageListener);
|
|
248
|
+
this.#messageListener = null;
|
|
249
|
+
}
|
|
250
|
+
this.#destroyEnclave();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#createContainer(): HTMLDialogElement {
|
|
254
|
+
const container = document.createElement("dialog");
|
|
255
|
+
Object.assign(container.style, {
|
|
256
|
+
position: "fixed",
|
|
257
|
+
inset: "0",
|
|
258
|
+
width: "100%",
|
|
259
|
+
height: "100%",
|
|
260
|
+
maxWidth: "100%",
|
|
261
|
+
maxHeight: "100%",
|
|
262
|
+
margin: "0",
|
|
263
|
+
padding: "0",
|
|
264
|
+
border: "none",
|
|
265
|
+
zIndex: "99999",
|
|
266
|
+
background: "#0a0a0a",
|
|
267
|
+
});
|
|
268
|
+
document.body.appendChild(container);
|
|
269
|
+
return container;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#ensureEnclave(): Promise<void> {
|
|
273
|
+
if (this.#iframe?.contentWindow) {
|
|
274
|
+
return Promise.resolve();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return new Promise<void>((resolve, reject) => {
|
|
278
|
+
const timer = setTimeout(() => {
|
|
279
|
+
reject(new Error("FaceSign enclave failed to load"));
|
|
280
|
+
}, PROPOSAL_TIMEOUT_MS);
|
|
281
|
+
|
|
282
|
+
if (!this.#container) {
|
|
283
|
+
this.#container = this.#createContainer();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this.#iframe = document.createElement("iframe");
|
|
287
|
+
this.#iframe.src = this.#enclaveUrl;
|
|
288
|
+
this.#iframe.title = "FaceSign Enclave";
|
|
289
|
+
this.#iframe.allow = "camera; fullscreen";
|
|
290
|
+
this.#iframe.style.cssText = "width:100%;height:100%;border:none;";
|
|
291
|
+
|
|
292
|
+
this.#container.appendChild(this.#iframe);
|
|
293
|
+
|
|
294
|
+
this.#resolveOpenEnclave = () => {
|
|
295
|
+
console.log("resolveOpenEnclave");
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
resolve();
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#showEnclave(): void {
|
|
303
|
+
const el = this.#container;
|
|
304
|
+
|
|
305
|
+
if (el && !el.open) {
|
|
306
|
+
el.showModal();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#hideEnclave(): void {
|
|
311
|
+
const el = this.#container;
|
|
312
|
+
if (el?.open) {
|
|
313
|
+
el.close();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#destroyEnclave(): void {
|
|
318
|
+
if (this.#iframe) {
|
|
319
|
+
this.#iframe.remove();
|
|
320
|
+
this.#iframe = null;
|
|
321
|
+
}
|
|
322
|
+
if (this.#container) {
|
|
323
|
+
this.#container.remove();
|
|
324
|
+
this.#container = null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type { Store } from "@idos-network/utils/store";
|
|
2
|
+
import type {
|
|
3
|
+
Wallet as NearWallet,
|
|
4
|
+
SignedMessage,
|
|
5
|
+
SignMessageParams,
|
|
6
|
+
} from "@near-wallet-selector/core";
|
|
7
|
+
import type { connect as connectT } from "near-api-js";
|
|
8
|
+
import type { AccessKeyList } from "near-api-js/lib/providers/provider";
|
|
9
|
+
|
|
10
|
+
import { KwilSigner } from "@idos-network/kwil-js";
|
|
11
|
+
import {
|
|
12
|
+
base64Decode,
|
|
13
|
+
binaryWriteUint16BE,
|
|
14
|
+
borshSerialize,
|
|
15
|
+
bs58Decode,
|
|
16
|
+
bytesConcat,
|
|
17
|
+
hexEncode,
|
|
18
|
+
utf8Decode,
|
|
19
|
+
} from "@idos-network/utils/codecs";
|
|
20
|
+
|
|
21
|
+
import type { KwilActionClient } from "../create-kwil-client";
|
|
22
|
+
|
|
23
|
+
import { getNearConnectionConfig } from "./get-config";
|
|
24
|
+
|
|
25
|
+
const NEAR_WALLET_TYPES: string[] = [
|
|
26
|
+
"browser",
|
|
27
|
+
"injected",
|
|
28
|
+
"instant-link",
|
|
29
|
+
"hardware",
|
|
30
|
+
"bridge",
|
|
31
|
+
] satisfies NearWallet["type"][];
|
|
32
|
+
|
|
33
|
+
export function looksLikeNearWallet(signer: unknown): signer is NearWallet {
|
|
34
|
+
return (
|
|
35
|
+
signer !== null &&
|
|
36
|
+
typeof signer === "object" &&
|
|
37
|
+
"id" in signer &&
|
|
38
|
+
"metadata" in signer &&
|
|
39
|
+
"type" in signer &&
|
|
40
|
+
typeof signer.type === "string" &&
|
|
41
|
+
NEAR_WALLET_TYPES.includes(signer.type)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class KwilNonce {
|
|
46
|
+
bytes: Uint8Array;
|
|
47
|
+
|
|
48
|
+
constructor(length = 32) {
|
|
49
|
+
// We're in the browser, so there's a window.crypto for sure.
|
|
50
|
+
/* global crypto */
|
|
51
|
+
this.bytes = crypto.getRandomValues(new Uint8Array(length));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get clampUTF8() {
|
|
55
|
+
return this.bytes.map((byte) => byte & 127);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a signer function that can be used to sign messages with a NEAR wallet
|
|
61
|
+
*/
|
|
62
|
+
function createNearWalletSigner(
|
|
63
|
+
wallet: NearWallet,
|
|
64
|
+
recipient: string,
|
|
65
|
+
): (message: string | Uint8Array) => Promise<Uint8Array> {
|
|
66
|
+
return async (message: string | Uint8Array): Promise<Uint8Array> => {
|
|
67
|
+
const messageString = typeof message === "string" ? message : utf8Decode(message);
|
|
68
|
+
|
|
69
|
+
if (!wallet.signMessage) {
|
|
70
|
+
throw new Error("Only wallets with signMessage are supported.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const nonceSuggestion = Buffer.from(new KwilNonce(32).bytes);
|
|
74
|
+
|
|
75
|
+
const {
|
|
76
|
+
nonce = nonceSuggestion,
|
|
77
|
+
signature,
|
|
78
|
+
// @ts-expect-error Signatures don't seem to be updated for NEP413 yet.
|
|
79
|
+
callbackUrl,
|
|
80
|
+
// oxlint-disable-next-line typescript/no-non-null-assertion -- Only non-signing wallets return void.
|
|
81
|
+
} = (await (
|
|
82
|
+
wallet.signMessage as (
|
|
83
|
+
_: SignMessageParams,
|
|
84
|
+
) => Promise<SignedMessage & { nonce?: Uint8Array }>
|
|
85
|
+
)({
|
|
86
|
+
message: messageString,
|
|
87
|
+
recipient,
|
|
88
|
+
nonce: nonceSuggestion,
|
|
89
|
+
}))!;
|
|
90
|
+
|
|
91
|
+
const nep413BorschSchema = {
|
|
92
|
+
struct: {
|
|
93
|
+
tag: "u32",
|
|
94
|
+
message: "string",
|
|
95
|
+
nonce: { array: { type: "u8", len: 32 } },
|
|
96
|
+
recipient: "string",
|
|
97
|
+
callbackUrl: { option: "string" },
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const nep413BorshParams = {
|
|
102
|
+
tag: 2147484061,
|
|
103
|
+
message: messageString,
|
|
104
|
+
nonce: Array.from(nonce),
|
|
105
|
+
recipient,
|
|
106
|
+
callbackUrl,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const nep413BorshPayload = borshSerialize(nep413BorschSchema, nep413BorshParams);
|
|
110
|
+
const result = bytesConcat(
|
|
111
|
+
binaryWriteUint16BE(nep413BorshPayload.length),
|
|
112
|
+
nep413BorshPayload,
|
|
113
|
+
base64Decode(signature),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function implicitAddressFromPublicKey(publicKey: string): string {
|
|
121
|
+
const key_without_prefix = publicKey.replace(/^ed25519:/, "");
|
|
122
|
+
return hexEncode(bs58Decode(key_without_prefix));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function getNearFullAccessPublicKeys(
|
|
126
|
+
namedAddress: string,
|
|
127
|
+
): Promise<string[] | undefined> {
|
|
128
|
+
let connect: typeof connectT;
|
|
129
|
+
try {
|
|
130
|
+
connect = (await import("near-api-js")).connect;
|
|
131
|
+
} catch (_e) {
|
|
132
|
+
throw new Error("Can't load near-api-js");
|
|
133
|
+
}
|
|
134
|
+
const connectionConfig = getNearConnectionConfig(namedAddress);
|
|
135
|
+
const nearConnection = await connect(connectionConfig);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const response: AccessKeyList = await nearConnection.connection.provider.query({
|
|
139
|
+
request_type: "view_access_key_list",
|
|
140
|
+
finality: "final",
|
|
141
|
+
account_id: namedAddress,
|
|
142
|
+
});
|
|
143
|
+
return response.keys
|
|
144
|
+
.filter((element) => element.access_key.permission === "FullAccess")
|
|
145
|
+
?.map((i) => i.public_key);
|
|
146
|
+
} catch {
|
|
147
|
+
// `Near` failed if namedAddress contains uppercase symbols
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function signNearMessage(
|
|
153
|
+
wallet: NearWallet,
|
|
154
|
+
message: string,
|
|
155
|
+
recipient = "idos.network",
|
|
156
|
+
): Promise<string> {
|
|
157
|
+
const signer = createNearWalletSigner(wallet, recipient);
|
|
158
|
+
const signedPayload = await signer(message);
|
|
159
|
+
return hexEncode(signedPayload);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function createNearWalletKwilSigner(
|
|
163
|
+
wallet: NearWallet,
|
|
164
|
+
currentAddress: string,
|
|
165
|
+
store: Store,
|
|
166
|
+
kwilClient: KwilActionClient,
|
|
167
|
+
recipient = "idos.network",
|
|
168
|
+
): Promise<{ kwilSigner: KwilSigner; publicKey: string }> {
|
|
169
|
+
if (!wallet.signMessage) throw new Error("Only wallets with signMessage are supported.");
|
|
170
|
+
|
|
171
|
+
if (wallet.id === "my-near-wallet") {
|
|
172
|
+
const { accountId, signature, publicKey, error } = Object.fromEntries(
|
|
173
|
+
new URLSearchParams(window.location.hash.slice(1)).entries(),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (signature) {
|
|
177
|
+
await store.set("signer-address", accountId);
|
|
178
|
+
await store.set("signer-public-key", publicKey);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const signMessageOriginal = wallet.signMessage.bind(wallet);
|
|
182
|
+
|
|
183
|
+
wallet.signMessage = async ({
|
|
184
|
+
message,
|
|
185
|
+
recipient,
|
|
186
|
+
}: SignMessageParams): Promise<SignedMessage & { nonce?: Uint8Array }> => {
|
|
187
|
+
if (error) return Promise.reject();
|
|
188
|
+
|
|
189
|
+
const lastMessage = await store.get<string>("sign-last-message");
|
|
190
|
+
const lastNonce = await store.get<Uint8Array>("sign-last-nonce");
|
|
191
|
+
|
|
192
|
+
if (signature && message === lastMessage && lastNonce) {
|
|
193
|
+
const nonce = Buffer.from(lastNonce);
|
|
194
|
+
const callbackUrl = await store.get<string>("sign-last-url");
|
|
195
|
+
|
|
196
|
+
return Promise.resolve({
|
|
197
|
+
accountId: currentAddress,
|
|
198
|
+
publicKey,
|
|
199
|
+
signature,
|
|
200
|
+
nonce,
|
|
201
|
+
message,
|
|
202
|
+
callbackUrl,
|
|
203
|
+
} as SignedMessage);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const callbackUrl = window.location.href;
|
|
207
|
+
const nonce = Buffer.from(new KwilNonce(32).clampUTF8);
|
|
208
|
+
|
|
209
|
+
await store.set("sign-last-message", message);
|
|
210
|
+
await store.set("sign-last-nonce", Array.from(nonce));
|
|
211
|
+
await store.set("sign-last-url", callbackUrl);
|
|
212
|
+
|
|
213
|
+
signMessageOriginal({ message, nonce, recipient, callbackUrl });
|
|
214
|
+
|
|
215
|
+
return new Promise(() => ({}) as SignedMessage);
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const storedAddress = await store.get<string>("signer-address");
|
|
220
|
+
|
|
221
|
+
let publicKey = await store.get<string>("signer-public-key");
|
|
222
|
+
|
|
223
|
+
if (storedAddress !== currentAddress || !publicKey) {
|
|
224
|
+
store.reset();
|
|
225
|
+
// To avoid re-using the old signer's kgw cookie.
|
|
226
|
+
// When kwil-js supports multi cookies, we can remove this.
|
|
227
|
+
await kwilClient.client.auth.logoutKGW();
|
|
228
|
+
|
|
229
|
+
const message = "idOS authentication";
|
|
230
|
+
const nonce = Buffer.from(new KwilNonce(32).bytes);
|
|
231
|
+
const signResult = await wallet.signMessage({ message, recipient, nonce });
|
|
232
|
+
if (!signResult) throw new Error("signMessage returned no result");
|
|
233
|
+
({ publicKey } = signResult);
|
|
234
|
+
|
|
235
|
+
await store.set("signer-address", currentAddress);
|
|
236
|
+
await store.set("signer-public-key", publicKey);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const signer = createNearWalletSigner(wallet, recipient);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
kwilSigner: new KwilSigner(signer, implicitAddressFromPublicKey(publicKey), "nep413"),
|
|
243
|
+
publicKey,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Network configuration mapping
|
|
2
|
+
const NEAR_NETWORKS = {
|
|
3
|
+
mainnet: {
|
|
4
|
+
networkId: "mainnet",
|
|
5
|
+
nodeUrl: "https://rpc.mainnet.near.org",
|
|
6
|
+
},
|
|
7
|
+
testnet: {
|
|
8
|
+
networkId: "testnet",
|
|
9
|
+
nodeUrl: "https://rpc.testnet.near.org",
|
|
10
|
+
},
|
|
11
|
+
betanet: {
|
|
12
|
+
networkId: "betanet",
|
|
13
|
+
nodeUrl: "https://rpc.betanet.near.org",
|
|
14
|
+
},
|
|
15
|
+
shardnet: {
|
|
16
|
+
networkId: "shardnet",
|
|
17
|
+
nodeUrl: "https://rpc.shardnet.near.org",
|
|
18
|
+
},
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
// Function to detect NEAR network from account ID
|
|
22
|
+
export const detectNearNetwork = (accountId: string): keyof typeof NEAR_NETWORKS => {
|
|
23
|
+
if (accountId.endsWith(".near")) return "mainnet";
|
|
24
|
+
if (accountId.endsWith(".testnet")) return "testnet";
|
|
25
|
+
if (accountId.endsWith(".betanet")) return "betanet";
|
|
26
|
+
if (accountId.endsWith(".shardnet")) return "shardnet";
|
|
27
|
+
|
|
28
|
+
// Default to testnet for development, mainnet for production
|
|
29
|
+
return "testnet";
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Function to get connection config based on account ID
|
|
33
|
+
export const getNearConnectionConfig = (
|
|
34
|
+
accountId: string,
|
|
35
|
+
): { networkId: string; nodeUrl: string } => {
|
|
36
|
+
const network = detectNearNetwork(accountId);
|
|
37
|
+
const networkConfig = NEAR_NETWORKS[network];
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
networkId: networkConfig.networkId,
|
|
41
|
+
nodeUrl: networkConfig.nodeUrl,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { KeyPair } from "near-api-js";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
binaryWriteUint16BE,
|
|
5
|
+
borshSerialize,
|
|
6
|
+
bytesConcat,
|
|
7
|
+
sha256Hash,
|
|
8
|
+
utf8Decode,
|
|
9
|
+
} from "@idos-network/utils/codecs";
|
|
10
|
+
|
|
11
|
+
export function kwilNep413Signer(
|
|
12
|
+
recipient: string,
|
|
13
|
+
): (keyPair: KeyPair) => (messageBytes: Uint8Array) => Promise<Uint8Array> {
|
|
14
|
+
return (keyPair: KeyPair) =>
|
|
15
|
+
async (messageBytes: Uint8Array): Promise<Uint8Array> => {
|
|
16
|
+
const message = utf8Decode(messageBytes);
|
|
17
|
+
const nonceLength = 32;
|
|
18
|
+
const nonce = crypto.getRandomValues(new Uint8Array(nonceLength));
|
|
19
|
+
|
|
20
|
+
const nep413BorschSchema = {
|
|
21
|
+
struct: {
|
|
22
|
+
message: "string",
|
|
23
|
+
nonce: { array: { type: "u8", len: nonceLength } },
|
|
24
|
+
recipient: "string",
|
|
25
|
+
callbackUrl: { option: "string" },
|
|
26
|
+
},
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
const tag = 2147484061; // 2**31 + 413
|
|
30
|
+
|
|
31
|
+
const { signature } = keyPair.sign(
|
|
32
|
+
sha256Hash(
|
|
33
|
+
bytesConcat(
|
|
34
|
+
borshSerialize("u32", tag),
|
|
35
|
+
borshSerialize(nep413BorschSchema, { message, nonce, recipient }),
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const kwilNep413BorschSchema = {
|
|
41
|
+
struct: {
|
|
42
|
+
tag: "u32",
|
|
43
|
+
...nep413BorschSchema.struct,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const kwilNep413BorshParams = {
|
|
48
|
+
tag,
|
|
49
|
+
message,
|
|
50
|
+
nonce,
|
|
51
|
+
recipient,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const kwilNep413BorshPayload = borshSerialize(kwilNep413BorschSchema, kwilNep413BorshParams);
|
|
55
|
+
|
|
56
|
+
return bytesConcat(
|
|
57
|
+
binaryWriteUint16BE(kwilNep413BorshPayload.length),
|
|
58
|
+
kwilNep413BorshPayload,
|
|
59
|
+
signature,
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
}
|