@llayer-network/wallet-standard-util 1.1.2

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 (49) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +0 -0
  3. package/lib/cjs/commitment.js +26 -0
  4. package/lib/cjs/commitment.js.map +1 -0
  5. package/lib/cjs/endpoint.js +45 -0
  6. package/lib/cjs/endpoint.js.map +1 -0
  7. package/lib/cjs/index.js +21 -0
  8. package/lib/cjs/index.js.map +1 -0
  9. package/lib/cjs/package.json +1 -0
  10. package/lib/cjs/signIn.js +186 -0
  11. package/lib/cjs/signIn.js.map +1 -0
  12. package/lib/cjs/signMessage.js +22 -0
  13. package/lib/cjs/signMessage.js.map +1 -0
  14. package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  15. package/lib/cjs/util.js +44 -0
  16. package/lib/cjs/util.js.map +1 -0
  17. package/lib/esm/commitment.js +23 -0
  18. package/lib/esm/commitment.js.map +1 -0
  19. package/lib/esm/endpoint.js +40 -0
  20. package/lib/esm/endpoint.js.map +1 -0
  21. package/lib/esm/index.js +5 -0
  22. package/lib/esm/index.js.map +1 -0
  23. package/lib/esm/signIn.js +176 -0
  24. package/lib/esm/signIn.js.map +1 -0
  25. package/lib/esm/signMessage.js +18 -0
  26. package/lib/esm/signMessage.js.map +1 -0
  27. package/lib/esm/tsconfig.esm.tsbuildinfo +1 -0
  28. package/lib/esm/util.js +40 -0
  29. package/lib/esm/util.js.map +1 -0
  30. package/lib/types/commitment.d.ts +8 -0
  31. package/lib/types/commitment.d.ts.map +1 -0
  32. package/lib/types/endpoint.d.ts +18 -0
  33. package/lib/types/endpoint.d.ts.map +1 -0
  34. package/lib/types/index.d.ts +5 -0
  35. package/lib/types/index.d.ts.map +1 -0
  36. package/lib/types/signIn.d.ts +34 -0
  37. package/lib/types/signIn.d.ts.map +1 -0
  38. package/lib/types/signMessage.d.ts +15 -0
  39. package/lib/types/signMessage.d.ts.map +1 -0
  40. package/lib/types/util.d.ts +2 -0
  41. package/lib/types/util.d.ts.map +1 -0
  42. package/package.json +44 -0
  43. package/src/__tests__/signIn.ts +89 -0
  44. package/src/commitment.ts +27 -0
  45. package/src/endpoint.ts +39 -0
  46. package/src/index.ts +4 -0
  47. package/src/signIn.ts +185 -0
  48. package/src/signMessage.ts +33 -0
  49. package/src/util.ts +54 -0
package/src/signIn.ts ADDED
@@ -0,0 +1,185 @@
1
+ import type { SolanaSignInInput, SolanaSignInOutput } from '@llayer-network/wallet-standard-features';
2
+ import { verifyMessageSignature } from './signMessage.js';
3
+ import { arraysEqual } from './util.js';
4
+
5
+ /**
6
+ * TODO: docs
7
+ */
8
+ export function verifySignIn(input: SolanaSignInInput, output: SolanaSignInOutput): boolean {
9
+ const {
10
+ signedMessage,
11
+ signature,
12
+ account: { publicKey },
13
+ } = output;
14
+ const message = deriveSignInMessage(input, output);
15
+ return (
16
+ !!message && verifyMessageSignature({ message, signedMessage, signature, publicKey: publicKey as Uint8Array })
17
+ );
18
+ }
19
+
20
+ /**
21
+ * TODO: docs
22
+ */
23
+ export function deriveSignInMessage(input: SolanaSignInInput, output: SolanaSignInOutput): Uint8Array | null {
24
+ const text = deriveSignInMessageText(input, output);
25
+ if (!text) return null;
26
+ return new TextEncoder().encode(text);
27
+ }
28
+
29
+ /**
30
+ * TODO: docs
31
+ */
32
+ export function deriveSignInMessageText(input: SolanaSignInInput, output: SolanaSignInOutput): string | null {
33
+ const parsed = parseSignInMessage(output.signedMessage);
34
+ if (!parsed) return null;
35
+
36
+ if (input.domain && input.domain !== parsed.domain) return null;
37
+ if (input.address && input.address !== parsed.address) return null;
38
+ if (input.statement !== parsed.statement) return null;
39
+ if (input.uri !== parsed.uri) return null;
40
+ if (input.version !== parsed.version) return null;
41
+ if (input.chainId !== parsed.chainId) return null;
42
+ if (input.nonce !== parsed.nonce) return null;
43
+ if (input.issuedAt !== parsed.issuedAt) return null;
44
+ if (input.expirationTime !== parsed.expirationTime) return null;
45
+ if (input.notBefore !== parsed.notBefore) return null;
46
+ if (input.requestId !== parsed.requestId) return null;
47
+ if (input.resources) {
48
+ if (!parsed.resources) return null;
49
+ if (!arraysEqual(input.resources, parsed.resources)) return null;
50
+ } else if (parsed.resources) return null;
51
+
52
+ return createSignInMessageText(parsed);
53
+ }
54
+
55
+ /**
56
+ * TODO: docs
57
+ */
58
+ export type SolanaSignInInputWithRequiredFields = SolanaSignInInput &
59
+ Required<Pick<SolanaSignInInput, 'domain' | 'address'>>;
60
+
61
+ /**
62
+ * TODO: docs
63
+ */
64
+ export function parseSignInMessage(message: Uint8Array): SolanaSignInInputWithRequiredFields | null {
65
+ const text = new TextDecoder().decode(message);
66
+ return parseSignInMessageText(text);
67
+ }
68
+
69
+ // TODO: implement https://github.com/solana-labs/solana/blob/master/docs/src/proposals/off-chain-message-signing.md
70
+ const DOMAIN = '(?<domain>[^\\n]+?) wants you to sign in with your Solana account:\\n';
71
+ const ADDRESS = '(?<address>[^\\n]+)(?:\\n|$)';
72
+ const STATEMENT = '(?:\\n(?<statement>[\\S\\s]*?)(?:\\n|$))??';
73
+ const URI = '(?:\\nURI: (?<uri>[^\\n]+))?';
74
+ const VERSION = '(?:\\nVersion: (?<version>[^\\n]+))?';
75
+ const CHAIN_ID = '(?:\\nChain ID: (?<chainId>[^\\n]+))?';
76
+ const NONCE = '(?:\\nNonce: (?<nonce>[^\\n]+))?';
77
+ const ISSUED_AT = '(?:\\nIssued At: (?<issuedAt>[^\\n]+))?';
78
+ const EXPIRATION_TIME = '(?:\\nExpiration Time: (?<expirationTime>[^\\n]+))?';
79
+ const NOT_BEFORE = '(?:\\nNot Before: (?<notBefore>[^\\n]+))?';
80
+ const REQUEST_ID = '(?:\\nRequest ID: (?<requestId>[^\\n]+))?';
81
+ const RESOURCES = '(?:\\nResources:(?<resources>(?:\\n- [^\\n]+)*))?';
82
+ const FIELDS = `${URI}${VERSION}${CHAIN_ID}${NONCE}${ISSUED_AT}${EXPIRATION_TIME}${NOT_BEFORE}${REQUEST_ID}${RESOURCES}`;
83
+ const MESSAGE = new RegExp(`^${DOMAIN}${ADDRESS}${STATEMENT}${FIELDS}\\n*$`);
84
+
85
+ /**
86
+ * TODO: docs
87
+ */
88
+ export function parseSignInMessageText(text: string): SolanaSignInInputWithRequiredFields | null {
89
+ const match = MESSAGE.exec(text);
90
+ if (!match) return null;
91
+ const groups = match.groups;
92
+ if (!groups) return null;
93
+
94
+ return {
95
+ domain: groups.domain!,
96
+
97
+ address: groups.address!,
98
+ statement: groups.statement,
99
+ uri: groups.uri,
100
+ version: groups.version,
101
+ nonce: groups.nonce,
102
+ chainId: groups.chainId,
103
+ issuedAt: groups.issuedAt,
104
+ expirationTime: groups.expirationTime,
105
+ notBefore: groups.notBefore,
106
+ requestId: groups.requestId,
107
+ resources: groups.resources?.split('\n- ').slice(1),
108
+ };
109
+ }
110
+
111
+ /**
112
+ * TODO: docs
113
+ */
114
+ export function createSignInMessage(input: SolanaSignInInputWithRequiredFields): Uint8Array {
115
+ const text = createSignInMessageText(input);
116
+ return new TextEncoder().encode(text);
117
+ }
118
+
119
+ /**
120
+ * TODO: docs
121
+ */
122
+ export function createSignInMessageText(input: SolanaSignInInputWithRequiredFields): string {
123
+ // ${domain} wants you to sign in with your Solana account:
124
+ // ${address}
125
+ //
126
+ // ${statement}
127
+ //
128
+ // URI: ${uri}
129
+ // Version: ${version}
130
+ // Chain ID: ${chain}
131
+ // Nonce: ${nonce}
132
+ // Issued At: ${issued-at}
133
+ // Expiration Time: ${expiration-time}
134
+ // Not Before: ${not-before}
135
+ // Request ID: ${request-id}
136
+ // Resources:
137
+ // - ${resources[0]}
138
+ // - ${resources[1]}
139
+ // ...
140
+ // - ${resources[n]}
141
+
142
+ let message = `${input.domain} wants you to sign in with your Solana account:\n`;
143
+ message += `${input.address}`;
144
+
145
+ if (input.statement) {
146
+ message += `\n\n${input.statement}`;
147
+ }
148
+
149
+ const fields: string[] = [];
150
+ if (input.uri) {
151
+ fields.push(`URI: ${input.uri}`);
152
+ }
153
+ if (input.version) {
154
+ fields.push(`Version: ${input.version}`);
155
+ }
156
+ if (input.chainId) {
157
+ fields.push(`Chain ID: ${input.chainId}`);
158
+ }
159
+ if (input.nonce) {
160
+ fields.push(`Nonce: ${input.nonce}`);
161
+ }
162
+ if (input.issuedAt) {
163
+ fields.push(`Issued At: ${input.issuedAt}`);
164
+ }
165
+ if (input.expirationTime) {
166
+ fields.push(`Expiration Time: ${input.expirationTime}`);
167
+ }
168
+ if (input.notBefore) {
169
+ fields.push(`Not Before: ${input.notBefore}`);
170
+ }
171
+ if (input.requestId) {
172
+ fields.push(`Request ID: ${input.requestId}`);
173
+ }
174
+ if (input.resources) {
175
+ fields.push(`Resources:`);
176
+ for (const resource of input.resources) {
177
+ fields.push(`- ${resource}`);
178
+ }
179
+ }
180
+ if (fields.length) {
181
+ message += `\n\n${fields.join('\n')}`;
182
+ }
183
+
184
+ return message;
185
+ }
@@ -0,0 +1,33 @@
1
+ import { ed25519 } from '@noble/curves/ed25519';
2
+ import type { SolanaSignMessageInput, SolanaSignMessageOutput } from '@llayer-network/wallet-standard-features';
3
+ import { bytesEqual } from './util.js';
4
+
5
+ /**
6
+ * TODO: docs
7
+ */
8
+ export function verifyMessageSignature({
9
+ message,
10
+ signedMessage,
11
+ signature,
12
+ publicKey,
13
+ }: {
14
+ message: Uint8Array;
15
+ signedMessage: Uint8Array;
16
+ signature: Uint8Array;
17
+ publicKey: Uint8Array;
18
+ }): boolean {
19
+ // TODO: implement https://github.com/solana-labs/solana/blob/master/docs/src/proposals/off-chain-message-signing.md
20
+ return bytesEqual(message, signedMessage) && ed25519.verify(signature, signedMessage, publicKey);
21
+ }
22
+
23
+ /**
24
+ * TODO: docs
25
+ */
26
+ export function verifySignMessage(input: SolanaSignMessageInput, output: SolanaSignMessageOutput): boolean {
27
+ const {
28
+ message,
29
+ account: { publicKey },
30
+ } = input;
31
+ const { signedMessage, signature } = output;
32
+ return verifyMessageSignature({ message, signedMessage, signature, publicKey: publicKey as Uint8Array });
33
+ }
package/src/util.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @internal
3
+ *
4
+ * Type with a numeric `length` and numerically indexed elements of a generic type `T`.
5
+ *
6
+ * For example, `Array<T>` and `Uint8Array`.
7
+ *
8
+ * @group Internal
9
+ */
10
+ export interface Indexed<T> {
11
+ length: number;
12
+ [index: number]: T;
13
+ }
14
+
15
+ /**
16
+ * @internal
17
+ *
18
+ * Efficiently compare {@link Indexed} arrays (e.g. `Array` and `Uint8Array`).
19
+ *
20
+ * @param a An array.
21
+ * @param b Another array.
22
+ *
23
+ * @return `true` if the arrays have the same length and elements, `false` otherwise.
24
+ *
25
+ * @group Internal
26
+ */
27
+ export function arraysEqual<T>(a: Indexed<T>, b: Indexed<T>): boolean {
28
+ if (a === b) return true;
29
+
30
+ const length = a.length;
31
+ if (length !== b.length) return false;
32
+
33
+ for (let i = 0; i < length; i++) {
34
+ if (a[i] !== b[i]) return false;
35
+ }
36
+
37
+ return true;
38
+ }
39
+
40
+ /**
41
+ * @internal
42
+ *
43
+ * Efficiently compare byte arrays, using {@link arraysEqual}.
44
+ *
45
+ * @param a A byte array.
46
+ * @param b Another byte array.
47
+ *
48
+ * @return `true` if the byte arrays have the same length and bytes, `false` otherwise.
49
+ *
50
+ * @group Internal
51
+ */
52
+ export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
53
+ return arraysEqual(a, b);
54
+ }