@cloakely/sdk 0.8.0 → 0.8.1
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/dist/private/client.d.ts +2 -0
- package/dist/private/client.js +5 -0
- package/dist/private/links.d.ts +20 -0
- package/dist/private/links.js +90 -7
- package/package.json +1 -1
package/dist/private/client.d.ts
CHANGED
|
@@ -104,6 +104,8 @@ export declare class CloakPrivate {
|
|
|
104
104
|
private state;
|
|
105
105
|
/** Pull new leaves from the pool's events, trial-decrypt them, and refresh spent flags. */
|
|
106
106
|
sync(pool: Address): Promise<OwnedNote[]>;
|
|
107
|
+
/** Every note this account has received in `pool`, spent ones included (each with `spent` and its nullifier). */
|
|
108
|
+
history(pool: Address): Promise<OwnedNote[]>;
|
|
107
109
|
balance(pool: Address): Promise<bigint>;
|
|
108
110
|
/** The most a single relayed call of this kind can move from `pool`: the balance minus the relayer's fee. */
|
|
109
111
|
maxSpendable(pool: Address, kind?: RelayKind): Promise<bigint>;
|
package/dist/private/client.js
CHANGED
|
@@ -249,6 +249,11 @@ export class CloakPrivate {
|
|
|
249
249
|
s.notes.forEach((x, i) => (x.spent = Boolean(flags[i])));
|
|
250
250
|
return s.notes.filter((x) => !x.spent);
|
|
251
251
|
}
|
|
252
|
+
/** Every note this account has received in `pool`, spent ones included (each with `spent` and its nullifier). */
|
|
253
|
+
async history(pool) {
|
|
254
|
+
await this.sync(pool);
|
|
255
|
+
return [...this.state(pool).notes];
|
|
256
|
+
}
|
|
252
257
|
async balance(pool) {
|
|
253
258
|
return (await this.sync(pool)).reduce((a, x) => a + x.amount, 0n);
|
|
254
259
|
}
|
package/dist/private/links.d.ts
CHANGED
|
@@ -22,6 +22,18 @@ export declare const newLinkSecret: () => Uint8Array;
|
|
|
22
22
|
export declare function linkKeys(secret: Uint8Array, passcode?: string): Promise<CloakKeys>;
|
|
23
23
|
/** The cloak: address a link's money is sent to. */
|
|
24
24
|
export declare function linkAddress(keys: CloakKeys): Promise<string>;
|
|
25
|
+
/** The 8-hex-digit check a locked link carries: the first 4 bytes of a hash of the account the passcode opens. */
|
|
26
|
+
export declare function linkCheck(keys: CloakKeys): Promise<string>;
|
|
27
|
+
/** Thrown when a link's check says the passcode is wrong. */
|
|
28
|
+
export declare class WrongPasscodeError extends Error {
|
|
29
|
+
constructor(riddle: boolean);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Opens a link's account. A locked link with a check tries the code (and the ways a phone may have mangled it) and
|
|
33
|
+
* throws WrongPasscodeError if none matches, without touching the network. A link without a check (made before
|
|
34
|
+
* checks existed) opens with the code as typed; an empty account is then all a wrong code looks like.
|
|
35
|
+
*/
|
|
36
|
+
export declare function openLinkKeys(l: ClaimLink, passcode?: string): Promise<CloakKeys>;
|
|
25
37
|
export interface ClaimLink {
|
|
26
38
|
secret: Uint8Array;
|
|
27
39
|
/** Block the link was funded in. Shown to the claimer; scanning still starts at the vaults' first block, because
|
|
@@ -40,7 +52,15 @@ export interface ClaimLink {
|
|
|
40
52
|
hint?: string;
|
|
41
53
|
/** Split into this many equal shares: each person who opens the link claims one (a red envelope). */
|
|
42
54
|
shares?: number;
|
|
55
|
+
/** A short check of the account a passcode opens, so a wrong passcode is caught on the device, before any
|
|
56
|
+
* network call. It tells a guesser nothing a copy of the link didn't already: each guess still costs a full scrypt. */
|
|
57
|
+
check?: string;
|
|
43
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A claim link: https://cloak.boblabs.tech/claim#c<letters and digits>. Everything after the # is one run of letters
|
|
61
|
+
* and digits (base 62), because chat apps end a link at characters like "-", "_", "." or "%" (WhatsApp and Telegram
|
|
62
|
+
* both cut links that way) and a cut link loses the money's key.
|
|
63
|
+
*/
|
|
44
64
|
export declare function buildClaimLink(l: ClaimLink, origin?: string): string;
|
|
45
65
|
/** Reads a claim link (the whole URL or just its fragment). Throws on anything that isn't one. */
|
|
46
66
|
export declare function parseClaimLink(link: string): ClaimLink;
|
package/dist/private/links.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { scryptAsync } from "@noble/hashes/scrypt";
|
|
23
23
|
import { randomBytes } from "@noble/hashes/utils";
|
|
24
|
-
import { concat, isAddress, keccak256, toBytes } from "viem";
|
|
24
|
+
import { concat, isAddress, keccak256, toBytes, toHex } from "viem";
|
|
25
25
|
import { CloakKeys, formatCloakAddress, parseCloakAddress } from "./crypto.js";
|
|
26
26
|
export const LINK_ORIGIN = "https://cloak.boblabs.tech";
|
|
27
27
|
export const LINK_MESSAGE_MAX = 140;
|
|
@@ -64,9 +64,46 @@ export async function linkKeys(secret, passcode) {
|
|
|
64
64
|
export async function linkAddress(keys) {
|
|
65
65
|
return formatCloakAddress((await keys.main()).pubKey, keys.encPub);
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
/** The 8-hex-digit check a locked link carries: the first 4 bytes of a hash of the account the passcode opens. */
|
|
68
|
+
export async function linkCheck(keys) {
|
|
69
|
+
return keccak256(concat([toBytes("cloak-link-check-v1"), toBytes(await linkAddress(keys))])).slice(2, 10);
|
|
70
|
+
}
|
|
71
|
+
/** Thrown when a link's check says the passcode is wrong. */
|
|
72
|
+
export class WrongPasscodeError extends Error {
|
|
73
|
+
constructor(riddle) {
|
|
74
|
+
super(riddle ? "That's not the answer. Check the spelling and try again." : "That's not the passcode. Check it with the sender and try again.");
|
|
75
|
+
this.name = "WrongPasscodeError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Ways a phone may have typed the same code: curly quotes and long dashes from autocorrect, a trailing full stop. */
|
|
79
|
+
function passcodeVariants(code) {
|
|
80
|
+
const base = normalizePasscode(code);
|
|
81
|
+
const straight = base.replace(/[\u2018\u2019\u201b\u2032]/g, "'").replace(/[\u201c\u201d\u2033]/g, '"').replace(/[\u2013\u2014\u2212]/g, "-");
|
|
82
|
+
return [...new Set([base, straight, straight.replace(/[.!?]+$/, "")])].filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Opens a link's account. A locked link with a check tries the code (and the ways a phone may have mangled it) and
|
|
86
|
+
* throws WrongPasscodeError if none matches, without touching the network. A link without a check (made before
|
|
87
|
+
* checks existed) opens with the code as typed; an empty account is then all a wrong code looks like.
|
|
88
|
+
*/
|
|
89
|
+
export async function openLinkKeys(l, passcode) {
|
|
90
|
+
if (!l.locked)
|
|
91
|
+
return linkKeys(l.secret);
|
|
92
|
+
if (!passcode || !normalizePasscode(passcode))
|
|
93
|
+
throw new Error(l.hint ? "Type the answer." : "Enter the passcode.");
|
|
94
|
+
if (!l.check)
|
|
95
|
+
return linkKeys(l.secret, passcode);
|
|
96
|
+
for (const v of passcodeVariants(passcode)) {
|
|
97
|
+
const k = await linkKeys(l.secret, v);
|
|
98
|
+
if ((await linkCheck(k)) === l.check)
|
|
99
|
+
return k;
|
|
100
|
+
}
|
|
101
|
+
throw new WrongPasscodeError(!!l.hint);
|
|
102
|
+
}
|
|
103
|
+
/** The fields of a claim link as query parameters (the secret in hex). */
|
|
104
|
+
function claimParams(l) {
|
|
68
105
|
const p = new URLSearchParams();
|
|
69
|
-
p.set("k",
|
|
106
|
+
p.set("k", toHex(l.secret).slice(2));
|
|
70
107
|
p.set("b", l.block.toString());
|
|
71
108
|
if (l.token)
|
|
72
109
|
p.set("t", l.token);
|
|
@@ -85,24 +122,69 @@ export function buildClaimLink(l, origin = LINK_ORIGIN) {
|
|
|
85
122
|
p.set("q", q);
|
|
86
123
|
if (l.shares && l.shares > 1)
|
|
87
124
|
p.set("n", String(Math.min(Math.floor(l.shares), LINK_SHARES_MAX)));
|
|
88
|
-
|
|
125
|
+
if (l.check && /^[0-9a-f]{8}$/.test(l.check))
|
|
126
|
+
p.set("c", l.check);
|
|
127
|
+
return p;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A claim link: https://cloak.boblabs.tech/claim#c<letters and digits>. Everything after the # is one run of letters
|
|
131
|
+
* and digits (base 62), because chat apps end a link at characters like "-", "_", "." or "%" (WhatsApp and Telegram
|
|
132
|
+
* both cut links that way) and a cut link loses the money's key.
|
|
133
|
+
*/
|
|
134
|
+
export function buildClaimLink(l, origin = LINK_ORIGIN) {
|
|
135
|
+
return `${origin.replace(/\/+$/, "")}/claim#c${toBase62(new TextEncoder().encode(claimParams(l).toString()))}`;
|
|
136
|
+
}
|
|
137
|
+
const B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
138
|
+
function toBase62(bytes) {
|
|
139
|
+
// a leading 1 byte keeps leading zero bytes; the payload is a few hundred bytes, so plain bigint math is fine
|
|
140
|
+
let n = BigInt(toHex(concat([new Uint8Array([1]), bytes])));
|
|
141
|
+
let out = "";
|
|
142
|
+
while (n > 0n) {
|
|
143
|
+
out = B62[Number(n % 62n)] + out;
|
|
144
|
+
n /= 62n;
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function fromBase62(s) {
|
|
149
|
+
let n = 0n;
|
|
150
|
+
for (const ch of s) {
|
|
151
|
+
const v = B62.indexOf(ch);
|
|
152
|
+
if (v < 0)
|
|
153
|
+
throw new Error("bad link");
|
|
154
|
+
n = n * 62n + BigInt(v);
|
|
155
|
+
}
|
|
156
|
+
let hex = n.toString(16);
|
|
157
|
+
if (hex.length % 2)
|
|
158
|
+
hex = "0" + hex;
|
|
159
|
+
const b = toBytes(`0x${hex}`);
|
|
160
|
+
if (b[0] !== 1)
|
|
161
|
+
throw new Error("bad link");
|
|
162
|
+
return b.slice(1);
|
|
89
163
|
}
|
|
90
164
|
/** Reads a claim link (the whole URL or just its fragment). Throws on anything that isn't one. */
|
|
91
165
|
export function parseClaimLink(link) {
|
|
92
|
-
|
|
166
|
+
let frag = (link.includes("#") ? link.slice(link.indexOf("#") + 1) : link).trim();
|
|
167
|
+
if (/^c[0-9A-Za-z]+$/.test(frag)) {
|
|
168
|
+
try {
|
|
169
|
+
frag = new TextDecoder("utf-8", { fatal: true }).decode(fromBase62(frag.slice(1)));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw new Error("This claim link is cut short or damaged. Ask the sender to copy the whole link again.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
93
175
|
const p = new URLSearchParams(frag);
|
|
94
176
|
const k = p.get("k");
|
|
95
177
|
if (!k)
|
|
96
178
|
throw new Error("This isn't a Cloak claim link.");
|
|
97
179
|
let secret;
|
|
98
180
|
try {
|
|
99
|
-
secret = unb64url(k);
|
|
181
|
+
secret = /^[0-9a-fA-F]{64}$/.test(k) ? toBytes(`0x${k}`) : unb64url(k);
|
|
100
182
|
}
|
|
101
183
|
catch {
|
|
102
184
|
throw new Error("This claim link is damaged. Ask the sender to copy it again.");
|
|
103
185
|
}
|
|
104
186
|
if (secret.length !== 32)
|
|
105
|
-
throw new Error("This claim link is cut short. Ask the sender to copy
|
|
187
|
+
throw new Error("This claim link is cut short. Ask the sender to copy the whole link again.");
|
|
106
188
|
const b = p.get("b") ?? "0";
|
|
107
189
|
const t = p.get("t");
|
|
108
190
|
const a = p.get("a");
|
|
@@ -116,6 +198,7 @@ export function parseClaimLink(link) {
|
|
|
116
198
|
locked: p.get("p") === "1",
|
|
117
199
|
hint: cleanLinkText(p.get("q"), LINK_HINT_MAX) || undefined,
|
|
118
200
|
shares: /^\d{1,2}$/.test(p.get("n") ?? "") && Number(p.get("n")) > 1 ? Math.min(Number(p.get("n")), LINK_SHARES_MAX) : undefined,
|
|
201
|
+
check: /^[0-9a-f]{8}$/.test(p.get("c") ?? "") ? p.get("c") : undefined,
|
|
119
202
|
};
|
|
120
203
|
}
|
|
121
204
|
/** Checks an amount and returns it as a plain decimal, or throws a plain-language error. Empty means "payer chooses". */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloakely/sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Cloak on Elysium: sealed-bid token launches and private trading (shielded balances, private swaps, batch auctions, private sends) with on-device zero-knowledge proofs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloak",
|