@swivy/web 0.0.0-stage → 1.0.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.
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Chain, TypedDataDescriptor } from "@swivy/core";
|
|
2
|
+
export interface SwivyBrowserManagedAccountProvisioning {
|
|
3
|
+
publicKey: string;
|
|
4
|
+
address: string;
|
|
5
|
+
accountType: "smart" | "managed";
|
|
6
|
+
serverShare: {
|
|
7
|
+
commitment: string;
|
|
8
|
+
encryptedPayload: string;
|
|
9
|
+
metadata: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
deviceShare: {
|
|
12
|
+
commitment: string;
|
|
13
|
+
metadata: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
recoveryShare: {
|
|
16
|
+
commitment: string;
|
|
17
|
+
metadata: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface SwivyLocalWalletShareRecord {
|
|
21
|
+
version: 1;
|
|
22
|
+
scheme: "shamir_gf256_v1";
|
|
23
|
+
accountId?: string;
|
|
24
|
+
chain: Chain;
|
|
25
|
+
x: number;
|
|
26
|
+
shareBase64: string;
|
|
27
|
+
publicKey: string;
|
|
28
|
+
address: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
}
|
|
31
|
+
export interface SwivyBrowserSplitWalletMaterial {
|
|
32
|
+
provisioning: SwivyBrowserManagedAccountProvisioning;
|
|
33
|
+
deviceShare: SwivyLocalWalletShareRecord;
|
|
34
|
+
recoveryShare: SwivyLocalWalletShareRecord;
|
|
35
|
+
recoveryPackage: string;
|
|
36
|
+
}
|
|
37
|
+
export interface SwivyLocalWalletStorageLike {
|
|
38
|
+
getItem(key: string): string | null;
|
|
39
|
+
setItem(key: string, value: string): void;
|
|
40
|
+
removeItem(key: string): void;
|
|
41
|
+
}
|
|
42
|
+
export declare function decodeRecoveryPackage(payload: string): SwivyLocalWalletShareRecord;
|
|
43
|
+
export declare function storeDeviceShare(storage: SwivyLocalWalletStorageLike, prefix: string, accountId: string, record: SwivyLocalWalletShareRecord): void;
|
|
44
|
+
export declare function loadDeviceShare(storage: SwivyLocalWalletStorageLike, prefix: string, accountId: string): SwivyLocalWalletShareRecord | null;
|
|
45
|
+
export declare function clearDeviceShare(storage: SwivyLocalWalletStorageLike, prefix: string, accountId: string): void;
|
|
46
|
+
export declare function createBrowserSplitWalletMaterial(chain: Chain, options?: {
|
|
47
|
+
label?: string;
|
|
48
|
+
}): Promise<SwivyBrowserSplitWalletMaterial>;
|
|
49
|
+
export declare function reconstructSecretFromShares(first: SwivyLocalWalletShareRecord, second: SwivyLocalWalletShareRecord): Uint8Array;
|
|
50
|
+
export declare function parseServerShareRecord(payload: string, localShare: SwivyLocalWalletShareRecord): Promise<SwivyLocalWalletShareRecord>;
|
|
51
|
+
export declare function exportPrivateKeyFromShares(chain: Chain, first: SwivyLocalWalletShareRecord, second: SwivyLocalWalletShareRecord): {
|
|
52
|
+
chain: Chain;
|
|
53
|
+
privateKey: string;
|
|
54
|
+
publicKey: string;
|
|
55
|
+
address: string;
|
|
56
|
+
};
|
|
57
|
+
export declare function signMessageLocallyWithShares(params: {
|
|
58
|
+
chain: Chain;
|
|
59
|
+
first: SwivyLocalWalletShareRecord;
|
|
60
|
+
second: SwivyLocalWalletShareRecord;
|
|
61
|
+
message?: string;
|
|
62
|
+
messageHex?: string;
|
|
63
|
+
messageBase64?: string;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
signature: string;
|
|
66
|
+
address: string;
|
|
67
|
+
publicKey: string;
|
|
68
|
+
}>;
|
|
69
|
+
export declare function signTypedDataLocallyWithShares(params: {
|
|
70
|
+
first: SwivyLocalWalletShareRecord;
|
|
71
|
+
second: SwivyLocalWalletShareRecord;
|
|
72
|
+
typedData: TypedDataDescriptor;
|
|
73
|
+
}): Promise<{
|
|
74
|
+
signature: string;
|
|
75
|
+
address: string;
|
|
76
|
+
publicKey: string;
|
|
77
|
+
}>;
|
|
78
|
+
export declare function signTransactionLocallyWithShares(params: {
|
|
79
|
+
chain: Chain;
|
|
80
|
+
first: SwivyLocalWalletShareRecord;
|
|
81
|
+
second: SwivyLocalWalletShareRecord;
|
|
82
|
+
transaction?: Record<string, unknown>;
|
|
83
|
+
serializedTransactionBase64?: string;
|
|
84
|
+
}): Promise<{
|
|
85
|
+
signedPayload: string;
|
|
86
|
+
address: string;
|
|
87
|
+
publicKey: string;
|
|
88
|
+
signature?: string;
|
|
89
|
+
}>;
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { Keypair, Transaction, VersionedTransaction } from "@solana/web3.js";
|
|
2
|
+
import bs58 from "bs58";
|
|
3
|
+
import { Wallet, getBytes } from "ethers";
|
|
4
|
+
import nacl from "tweetnacl";
|
|
5
|
+
const GF_REDUCTION = 0x1b;
|
|
6
|
+
function randomBytes(length) {
|
|
7
|
+
const buffer = new Uint8Array(length);
|
|
8
|
+
globalThis.crypto.getRandomValues(buffer);
|
|
9
|
+
return buffer;
|
|
10
|
+
}
|
|
11
|
+
function gfMultiply(left, right) {
|
|
12
|
+
let a = left & 0xff;
|
|
13
|
+
let b = right & 0xff;
|
|
14
|
+
let result = 0;
|
|
15
|
+
for (let index = 0; index < 8; index += 1) {
|
|
16
|
+
if (b & 1) {
|
|
17
|
+
result ^= a;
|
|
18
|
+
}
|
|
19
|
+
const highBit = a & 0x80;
|
|
20
|
+
a = (a << 1) & 0xff;
|
|
21
|
+
if (highBit) {
|
|
22
|
+
a ^= GF_REDUCTION;
|
|
23
|
+
}
|
|
24
|
+
b >>= 1;
|
|
25
|
+
}
|
|
26
|
+
return result & 0xff;
|
|
27
|
+
}
|
|
28
|
+
function gfPow(value, exponent) {
|
|
29
|
+
let result = 1;
|
|
30
|
+
let base = value & 0xff;
|
|
31
|
+
let power = exponent;
|
|
32
|
+
while (power > 0) {
|
|
33
|
+
if (power & 1) {
|
|
34
|
+
result = gfMultiply(result, base);
|
|
35
|
+
}
|
|
36
|
+
base = gfMultiply(base, base);
|
|
37
|
+
power >>= 1;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function gfInverse(value) {
|
|
42
|
+
if (value === 0) {
|
|
43
|
+
throw new Error("gf_zero_inverse");
|
|
44
|
+
}
|
|
45
|
+
return gfPow(value, 254);
|
|
46
|
+
}
|
|
47
|
+
function gfDivide(left, right) {
|
|
48
|
+
if (left === 0) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
return gfMultiply(left, gfInverse(right));
|
|
52
|
+
}
|
|
53
|
+
function bytesToBase64(bytes) {
|
|
54
|
+
if (typeof Buffer !== "undefined") {
|
|
55
|
+
return Buffer.from(bytes).toString("base64");
|
|
56
|
+
}
|
|
57
|
+
let binary = "";
|
|
58
|
+
for (const byte of bytes) {
|
|
59
|
+
binary += String.fromCharCode(byte);
|
|
60
|
+
}
|
|
61
|
+
return btoa(binary);
|
|
62
|
+
}
|
|
63
|
+
function base64ToBytes(value) {
|
|
64
|
+
if (typeof Buffer !== "undefined") {
|
|
65
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
66
|
+
}
|
|
67
|
+
const binary = atob(value);
|
|
68
|
+
const bytes = new Uint8Array(binary.length);
|
|
69
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
70
|
+
bytes[index] = binary.charCodeAt(index);
|
|
71
|
+
}
|
|
72
|
+
return bytes;
|
|
73
|
+
}
|
|
74
|
+
function bytesToHex(bytes) {
|
|
75
|
+
return Array.from(bytes)
|
|
76
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
77
|
+
.join("");
|
|
78
|
+
}
|
|
79
|
+
function toArrayBuffer(bytes) {
|
|
80
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
81
|
+
}
|
|
82
|
+
function utf8ToBase64(value) {
|
|
83
|
+
return bytesToBase64(new TextEncoder().encode(value));
|
|
84
|
+
}
|
|
85
|
+
function base64ToUtf8(value) {
|
|
86
|
+
return new TextDecoder().decode(base64ToBytes(value));
|
|
87
|
+
}
|
|
88
|
+
function bytesEqual(left, right) {
|
|
89
|
+
if (left.length !== right.length) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
93
|
+
if (left[index] !== right[index]) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
async function sha256Hex(value) {
|
|
100
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
101
|
+
return Array.from(new Uint8Array(digest))
|
|
102
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
103
|
+
.join("");
|
|
104
|
+
}
|
|
105
|
+
function splitSecret(secret) {
|
|
106
|
+
const coefficients = randomBytes(secret.length);
|
|
107
|
+
const shareIndexes = [1, 2, 3];
|
|
108
|
+
const shares = shareIndexes.map((x) => ({
|
|
109
|
+
x,
|
|
110
|
+
bytes: new Uint8Array(secret.length),
|
|
111
|
+
}));
|
|
112
|
+
for (let offset = 0; offset < secret.length; offset += 1) {
|
|
113
|
+
const constant = secret[offset] ?? 0;
|
|
114
|
+
const coefficient = coefficients[offset] ?? 0;
|
|
115
|
+
for (const share of shares) {
|
|
116
|
+
share.bytes[offset] = constant ^ gfMultiply(coefficient, share.x);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return shares;
|
|
120
|
+
}
|
|
121
|
+
function combineTwoShares(left, right) {
|
|
122
|
+
if (left.bytes.length !== right.bytes.length) {
|
|
123
|
+
throw new Error("share_length_mismatch");
|
|
124
|
+
}
|
|
125
|
+
if (left.x === right.x) {
|
|
126
|
+
throw new Error("share_index_conflict");
|
|
127
|
+
}
|
|
128
|
+
const recovered = new Uint8Array(left.bytes.length);
|
|
129
|
+
const leftDenominator = left.x ^ right.x;
|
|
130
|
+
const rightDenominator = right.x ^ left.x;
|
|
131
|
+
for (let offset = 0; offset < recovered.length; offset += 1) {
|
|
132
|
+
const leftTerm = gfMultiply(left.bytes[offset] ?? 0, gfDivide(right.x, leftDenominator));
|
|
133
|
+
const rightTerm = gfMultiply(right.bytes[offset] ?? 0, gfDivide(left.x, rightDenominator));
|
|
134
|
+
recovered[offset] = leftTerm ^ rightTerm;
|
|
135
|
+
}
|
|
136
|
+
return recovered;
|
|
137
|
+
}
|
|
138
|
+
function serializeShareRecord(record) {
|
|
139
|
+
return JSON.stringify(record);
|
|
140
|
+
}
|
|
141
|
+
function isChain(value) {
|
|
142
|
+
return value === "evm" || value === "solana";
|
|
143
|
+
}
|
|
144
|
+
function deserializeShareRecord(payload, errorCode = "invalid_share_record") {
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(payload);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new Error(errorCode);
|
|
151
|
+
}
|
|
152
|
+
if (!parsed || typeof parsed !== "object") {
|
|
153
|
+
throw new Error(errorCode);
|
|
154
|
+
}
|
|
155
|
+
const record = parsed;
|
|
156
|
+
if (record.version !== 1
|
|
157
|
+
|| record.scheme !== "shamir_gf256_v1"
|
|
158
|
+
|| !isChain(record.chain)
|
|
159
|
+
|| !Number.isInteger(record.x)
|
|
160
|
+
|| (record.x ?? 0) <= 0
|
|
161
|
+
|| typeof record.shareBase64 !== "string"
|
|
162
|
+
|| !record.shareBase64
|
|
163
|
+
|| typeof record.publicKey !== "string"
|
|
164
|
+
|| !record.publicKey
|
|
165
|
+
|| typeof record.address !== "string"
|
|
166
|
+
|| !record.address
|
|
167
|
+
|| typeof record.createdAt !== "string"
|
|
168
|
+
|| Number.isNaN(Date.parse(record.createdAt))
|
|
169
|
+
|| (record.accountId !== undefined && typeof record.accountId !== "string")) {
|
|
170
|
+
throw new Error(errorCode);
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
if (base64ToBytes(record.shareBase64).length === 0) {
|
|
174
|
+
throw new Error(errorCode);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new Error(errorCode);
|
|
179
|
+
}
|
|
180
|
+
return record;
|
|
181
|
+
}
|
|
182
|
+
function encodeRecoveryPackage(record) {
|
|
183
|
+
return utf8ToBase64(serializeShareRecord(record));
|
|
184
|
+
}
|
|
185
|
+
export function decodeRecoveryPackage(payload) {
|
|
186
|
+
try {
|
|
187
|
+
return deserializeShareRecord(base64ToUtf8(payload), "invalid_recovery_package");
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
throw new Error("invalid_recovery_package");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function createShareMetadata(x, chain, label) {
|
|
194
|
+
return {
|
|
195
|
+
scheme: "shamir_gf256_v1",
|
|
196
|
+
threshold: 2,
|
|
197
|
+
totalShares: 3,
|
|
198
|
+
shareIndex: x,
|
|
199
|
+
encoding: "json",
|
|
200
|
+
encrypted: false,
|
|
201
|
+
chain,
|
|
202
|
+
label,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function serializeShareKeyMaterial(record) {
|
|
206
|
+
return JSON.stringify({
|
|
207
|
+
version: record.version,
|
|
208
|
+
scheme: record.scheme,
|
|
209
|
+
chain: record.chain,
|
|
210
|
+
x: record.x,
|
|
211
|
+
shareBase64: record.shareBase64,
|
|
212
|
+
publicKey: record.publicKey,
|
|
213
|
+
address: record.address,
|
|
214
|
+
createdAt: record.createdAt,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async function deriveShareWrapKey(record) {
|
|
218
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(serializeShareKeyMaterial(record)));
|
|
219
|
+
return globalThis.crypto.subtle.importKey("raw", digest, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
220
|
+
}
|
|
221
|
+
async function encryptServerShareForLocalShare(payload, record) {
|
|
222
|
+
const key = await deriveShareWrapKey(record);
|
|
223
|
+
const iv = randomBytes(12);
|
|
224
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv: toArrayBuffer(iv) }, key, new TextEncoder().encode(payload));
|
|
225
|
+
return {
|
|
226
|
+
shareIndex: record.x,
|
|
227
|
+
ivBase64: bytesToBase64(iv),
|
|
228
|
+
ciphertextBase64: bytesToBase64(new Uint8Array(ciphertext)),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function decryptServerShareForLocalShare(payload, record) {
|
|
232
|
+
const entry = payload.entries.find((candidate) => candidate.shareIndex === record.x);
|
|
233
|
+
if (!entry) {
|
|
234
|
+
throw new Error("server_share_unwrap_not_available_for_local_share");
|
|
235
|
+
}
|
|
236
|
+
const key = await deriveShareWrapKey(record);
|
|
237
|
+
const plaintext = await globalThis.crypto.subtle.decrypt({ name: "AES-GCM", iv: toArrayBuffer(base64ToBytes(entry.ivBase64)) }, key, toArrayBuffer(base64ToBytes(entry.ciphertextBase64)));
|
|
238
|
+
const parsed = deserializeShareRecord(new TextDecoder().decode(new Uint8Array(plaintext)));
|
|
239
|
+
if (parsed.publicKey !== record.publicKey || parsed.address !== record.address) {
|
|
240
|
+
throw new Error("server_share_unwrap_identity_mismatch");
|
|
241
|
+
}
|
|
242
|
+
return parsed;
|
|
243
|
+
}
|
|
244
|
+
function parseWrappedServerSharePayload(payload) {
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(payload);
|
|
247
|
+
if (parsed?.version === 1
|
|
248
|
+
&& parsed?.scheme === "share_wrap_aes_gcm_v1"
|
|
249
|
+
&& Array.isArray(parsed.entries)
|
|
250
|
+
&& parsed.entries.every((entry) => typeof entry?.shareIndex === "number"
|
|
251
|
+
&& typeof entry?.ivBase64 === "string"
|
|
252
|
+
&& typeof entry?.ciphertextBase64 === "string")) {
|
|
253
|
+
return parsed;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
function getStorageKey(prefix, accountId) {
|
|
262
|
+
return `${prefix}:${accountId}`;
|
|
263
|
+
}
|
|
264
|
+
export function storeDeviceShare(storage, prefix, accountId, record) {
|
|
265
|
+
storage.setItem(getStorageKey(prefix, accountId), serializeShareRecord({ ...record, accountId }));
|
|
266
|
+
}
|
|
267
|
+
export function loadDeviceShare(storage, prefix, accountId) {
|
|
268
|
+
const storageKey = getStorageKey(prefix, accountId);
|
|
269
|
+
const stored = storage.getItem(storageKey);
|
|
270
|
+
if (!stored) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const record = deserializeShareRecord(stored);
|
|
275
|
+
if (record.accountId !== undefined && record.accountId !== accountId) {
|
|
276
|
+
throw new Error("device_share_account_mismatch");
|
|
277
|
+
}
|
|
278
|
+
return record;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
storage.removeItem(storageKey);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export function clearDeviceShare(storage, prefix, accountId) {
|
|
286
|
+
storage.removeItem(getStorageKey(prefix, accountId));
|
|
287
|
+
}
|
|
288
|
+
export async function createBrowserSplitWalletMaterial(chain, options) {
|
|
289
|
+
const createdAt = new Date().toISOString();
|
|
290
|
+
const label = options?.label ?? `${chain}-browser-wallet`;
|
|
291
|
+
let secretBytes;
|
|
292
|
+
let publicKey;
|
|
293
|
+
let address;
|
|
294
|
+
if (chain === "evm") {
|
|
295
|
+
const wallet = Wallet.createRandom();
|
|
296
|
+
secretBytes = getBytes(wallet.privateKey);
|
|
297
|
+
publicKey = wallet.signingKey.publicKey;
|
|
298
|
+
address = wallet.address;
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
const seed = randomBytes(32);
|
|
302
|
+
const keypair = Keypair.fromSeed(seed);
|
|
303
|
+
secretBytes = seed;
|
|
304
|
+
publicKey = keypair.publicKey.toBase58();
|
|
305
|
+
address = keypair.publicKey.toBase58();
|
|
306
|
+
}
|
|
307
|
+
const [serverShare, deviceShare, recoveryShare] = splitSecret(secretBytes).map((share) => ({
|
|
308
|
+
x: share.x,
|
|
309
|
+
record: {
|
|
310
|
+
version: 1,
|
|
311
|
+
scheme: "shamir_gf256_v1",
|
|
312
|
+
chain,
|
|
313
|
+
x: share.x,
|
|
314
|
+
shareBase64: bytesToBase64(share.bytes),
|
|
315
|
+
publicKey,
|
|
316
|
+
address,
|
|
317
|
+
createdAt,
|
|
318
|
+
},
|
|
319
|
+
}));
|
|
320
|
+
const serverPayload = serializeShareRecord(serverShare.record);
|
|
321
|
+
const wrappedServerPayload = JSON.stringify({
|
|
322
|
+
version: 1,
|
|
323
|
+
scheme: "share_wrap_aes_gcm_v1",
|
|
324
|
+
entries: await Promise.all([
|
|
325
|
+
encryptServerShareForLocalShare(serverPayload, deviceShare.record),
|
|
326
|
+
encryptServerShareForLocalShare(serverPayload, recoveryShare.record),
|
|
327
|
+
]),
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
provisioning: {
|
|
331
|
+
publicKey,
|
|
332
|
+
address,
|
|
333
|
+
accountType: "smart",
|
|
334
|
+
serverShare: {
|
|
335
|
+
commitment: await sha256Hex(serverPayload),
|
|
336
|
+
encryptedPayload: wrappedServerPayload,
|
|
337
|
+
metadata: {
|
|
338
|
+
...createShareMetadata(serverShare.x, chain, label),
|
|
339
|
+
encrypted: true,
|
|
340
|
+
wrappingScheme: "share_wrap_aes_gcm_v1",
|
|
341
|
+
decryptableWithShareIndexes: [deviceShare.x, recoveryShare.x],
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
deviceShare: {
|
|
345
|
+
commitment: await sha256Hex(serializeShareRecord(deviceShare.record)),
|
|
346
|
+
metadata: createShareMetadata(deviceShare.x, chain, label),
|
|
347
|
+
},
|
|
348
|
+
recoveryShare: {
|
|
349
|
+
commitment: await sha256Hex(serializeShareRecord(recoveryShare.record)),
|
|
350
|
+
metadata: createShareMetadata(recoveryShare.x, chain, label),
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
deviceShare: deviceShare.record,
|
|
354
|
+
recoveryShare: recoveryShare.record,
|
|
355
|
+
recoveryPackage: encodeRecoveryPackage(recoveryShare.record),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
export function reconstructSecretFromShares(first, second) {
|
|
359
|
+
if (first.chain !== second.chain) {
|
|
360
|
+
throw new Error("share_chain_mismatch");
|
|
361
|
+
}
|
|
362
|
+
if (first.address !== second.address || first.publicKey !== second.publicKey) {
|
|
363
|
+
throw new Error("share_identity_mismatch");
|
|
364
|
+
}
|
|
365
|
+
return combineTwoShares({ x: first.x, bytes: base64ToBytes(first.shareBase64) }, { x: second.x, bytes: base64ToBytes(second.shareBase64) });
|
|
366
|
+
}
|
|
367
|
+
export async function parseServerShareRecord(payload, localShare) {
|
|
368
|
+
const wrapped = parseWrappedServerSharePayload(payload);
|
|
369
|
+
if (wrapped) {
|
|
370
|
+
return decryptServerShareForLocalShare(wrapped, localShare);
|
|
371
|
+
}
|
|
372
|
+
const parsed = deserializeShareRecord(payload, "invalid_server_share_record");
|
|
373
|
+
if (parsed.publicKey !== localShare.publicKey || parsed.address !== localShare.address) {
|
|
374
|
+
throw new Error("server_share_identity_mismatch");
|
|
375
|
+
}
|
|
376
|
+
const localShareBytes = base64ToBytes(localShare.shareBase64);
|
|
377
|
+
const serverShareBytes = base64ToBytes(parsed.shareBase64);
|
|
378
|
+
if (bytesEqual(localShareBytes, serverShareBytes)) {
|
|
379
|
+
throw new Error("server_share_matches_local_share");
|
|
380
|
+
}
|
|
381
|
+
return parsed;
|
|
382
|
+
}
|
|
383
|
+
export function exportPrivateKeyFromShares(chain, first, second) {
|
|
384
|
+
const secret = reconstructSecretFromShares(first, second);
|
|
385
|
+
if (chain === "evm") {
|
|
386
|
+
const privateKey = `0x${bytesToHex(secret)}`;
|
|
387
|
+
const wallet = new Wallet(privateKey);
|
|
388
|
+
if (wallet.address !== first.address || wallet.signingKey.publicKey !== first.publicKey) {
|
|
389
|
+
throw new Error("reconstructed_wallet_mismatch");
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
chain,
|
|
393
|
+
privateKey,
|
|
394
|
+
publicKey: wallet.signingKey.publicKey,
|
|
395
|
+
address: wallet.address,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const keypair = Keypair.fromSeed(secret);
|
|
399
|
+
if (keypair.publicKey.toBase58() !== first.address || keypair.publicKey.toBase58() !== first.publicKey) {
|
|
400
|
+
throw new Error("reconstructed_wallet_mismatch");
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
chain,
|
|
404
|
+
privateKey: bs58.encode(keypair.secretKey),
|
|
405
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
406
|
+
address: keypair.publicKey.toBase58(),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
export async function signMessageLocallyWithShares(params) {
|
|
410
|
+
const exported = exportPrivateKeyFromShares(params.chain, params.first, params.second);
|
|
411
|
+
if (params.chain === "evm") {
|
|
412
|
+
const wallet = new Wallet(exported.privateKey);
|
|
413
|
+
const payload = params.messageHex
|
|
414
|
+
? getBytes(params.messageHex)
|
|
415
|
+
: params.messageBase64
|
|
416
|
+
? base64ToBytes(params.messageBase64)
|
|
417
|
+
: params.message ?? "";
|
|
418
|
+
return {
|
|
419
|
+
signature: await wallet.signMessage(payload),
|
|
420
|
+
address: wallet.address,
|
|
421
|
+
publicKey: wallet.signingKey.publicKey,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const secretKey = bs58.decode(exported.privateKey);
|
|
425
|
+
const payload = params.messageBase64
|
|
426
|
+
? base64ToBytes(params.messageBase64)
|
|
427
|
+
: new TextEncoder().encode(params.message ?? "");
|
|
428
|
+
const signature = nacl.sign.detached(payload, secretKey);
|
|
429
|
+
return {
|
|
430
|
+
signature: bytesToBase64(signature),
|
|
431
|
+
address: exported.address,
|
|
432
|
+
publicKey: exported.publicKey,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
export async function signTypedDataLocallyWithShares(params) {
|
|
436
|
+
const exported = exportPrivateKeyFromShares("evm", params.first, params.second);
|
|
437
|
+
const wallet = new Wallet(exported.privateKey);
|
|
438
|
+
return {
|
|
439
|
+
signature: await wallet.signTypedData(params.typedData.domain ?? {}, params.typedData.types, params.typedData.message),
|
|
440
|
+
address: wallet.address,
|
|
441
|
+
publicKey: wallet.signingKey.publicKey,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
export async function signTransactionLocallyWithShares(params) {
|
|
445
|
+
const exported = exportPrivateKeyFromShares(params.chain, params.first, params.second);
|
|
446
|
+
if (params.chain === "evm") {
|
|
447
|
+
if (!params.transaction) {
|
|
448
|
+
throw new Error("missing_evm_transaction");
|
|
449
|
+
}
|
|
450
|
+
const wallet = new Wallet(exported.privateKey);
|
|
451
|
+
return {
|
|
452
|
+
signedPayload: await wallet.signTransaction(params.transaction),
|
|
453
|
+
address: wallet.address,
|
|
454
|
+
publicKey: wallet.signingKey.publicKey,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (!params.serializedTransactionBase64) {
|
|
458
|
+
throw new Error("missing_solana_transaction");
|
|
459
|
+
}
|
|
460
|
+
const seed = reconstructSecretFromShares(params.first, params.second);
|
|
461
|
+
const keypair = Keypair.fromSeed(seed);
|
|
462
|
+
const serializedBytes = base64ToBytes(params.serializedTransactionBase64);
|
|
463
|
+
try {
|
|
464
|
+
const transaction = VersionedTransaction.deserialize(serializedBytes);
|
|
465
|
+
transaction.sign([keypair]);
|
|
466
|
+
const signature = transaction.signatures[0];
|
|
467
|
+
return {
|
|
468
|
+
signedPayload: bytesToBase64(transaction.serialize()),
|
|
469
|
+
address: keypair.publicKey.toBase58(),
|
|
470
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
471
|
+
signature: signature ? bs58.encode(signature) : undefined,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
const transaction = Transaction.from(serializedBytes);
|
|
476
|
+
transaction.partialSign(keypair);
|
|
477
|
+
const firstSignature = transaction.signatures.find((candidate) => candidate.signature)?.signature;
|
|
478
|
+
return {
|
|
479
|
+
signedPayload: bytesToBase64(transaction.serialize()),
|
|
480
|
+
address: keypair.publicKey.toBase58(),
|
|
481
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
482
|
+
signature: firstSignature ? bs58.encode(firstSignature) : undefined,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swivy/web",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Swivy browser SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/packages/web/src/index.js",
|
|
7
|
+
"types": "dist/packages/web/src/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run test/*.test.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@solana/web3.js": "^1.99.0",
|
|
14
|
+
"@simplewebauthn/browser": "^13.3.0",
|
|
15
|
+
"@swivy/core": "^1.0.0",
|
|
16
|
+
"bs58": "^6.0.0",
|
|
17
|
+
"ethers": "^6.17.0",
|
|
18
|
+
"tweetnacl": "^1.0.3"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/packages/web/src"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/packages/web/src/index.d.ts",
|
|
26
|
+
"import": "./dist/packages/web/src/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
|
+
},
|
|
36
|
+
"license": "ISC",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/Travisswop/Swivy.git",
|
|
40
|
+
"directory": "packages/web"
|
|
41
|
+
}
|
|
42
|
+
}
|