@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.
- package/LICENSE +202 -0
- package/README.md +0 -0
- package/lib/cjs/commitment.js +26 -0
- package/lib/cjs/commitment.js.map +1 -0
- package/lib/cjs/endpoint.js +45 -0
- package/lib/cjs/endpoint.js.map +1 -0
- package/lib/cjs/index.js +21 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/cjs/package.json +1 -0
- package/lib/cjs/signIn.js +186 -0
- package/lib/cjs/signIn.js.map +1 -0
- package/lib/cjs/signMessage.js +22 -0
- package/lib/cjs/signMessage.js.map +1 -0
- package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -0
- package/lib/cjs/util.js +44 -0
- package/lib/cjs/util.js.map +1 -0
- package/lib/esm/commitment.js +23 -0
- package/lib/esm/commitment.js.map +1 -0
- package/lib/esm/endpoint.js +40 -0
- package/lib/esm/endpoint.js.map +1 -0
- package/lib/esm/index.js +5 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/signIn.js +176 -0
- package/lib/esm/signIn.js.map +1 -0
- package/lib/esm/signMessage.js +18 -0
- package/lib/esm/signMessage.js.map +1 -0
- package/lib/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/lib/esm/util.js +40 -0
- package/lib/esm/util.js.map +1 -0
- package/lib/types/commitment.d.ts +8 -0
- package/lib/types/commitment.d.ts.map +1 -0
- package/lib/types/endpoint.d.ts +18 -0
- package/lib/types/endpoint.d.ts.map +1 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/signIn.d.ts +34 -0
- package/lib/types/signIn.d.ts.map +1 -0
- package/lib/types/signMessage.d.ts +15 -0
- package/lib/types/signMessage.d.ts.map +1 -0
- package/lib/types/util.d.ts +2 -0
- package/lib/types/util.d.ts.map +1 -0
- package/package.json +44 -0
- package/src/__tests__/signIn.ts +89 -0
- package/src/commitment.ts +27 -0
- package/src/endpoint.ts +39 -0
- package/src/index.ts +4 -0
- package/src/signIn.ts +185 -0
- package/src/signMessage.ts +33 -0
- 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
|
+
}
|