@cloakely/sdk 0.7.1 → 0.8.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/dist/private/client.d.ts +14 -0
- package/dist/private/client.js +66 -2
- package/dist/private/index.d.ts +1 -0
- package/dist/private/index.js +1 -0
- package/dist/private/links.d.ts +65 -0
- package/dist/private/links.js +186 -0
- package/dist/private/shared.js +4 -2
- package/package.json +1 -1
package/dist/private/client.d.ts
CHANGED
|
@@ -248,6 +248,20 @@ export declare class CloakPrivate {
|
|
|
248
248
|
send(token: Address, to: string, amount: bigint, onStatus?: Status): Promise<Hex>;
|
|
249
249
|
/** Private tokens (a launch token, or an asset such as USDC) → any address. */
|
|
250
250
|
unshieldToken(token: Address, to: Address, amount: bigint, onStatus?: Status): Promise<Hex>;
|
|
251
|
+
/**
|
|
252
|
+
* Move everything this account holds of `token` to `to` in one relayed call, leaving no change behind: to a cloak:
|
|
253
|
+
* address (stays private) or out to a 0x address (a withdrawal; HYPE arrives as native HYPE, minus the withdrawal
|
|
254
|
+
* fee). The relayer's fee comes out of the amount. Spends the two largest notes of one tree epoch, so an account
|
|
255
|
+
* with more notes than that needs another call. Claim links use this, so a claimed link is left empty. With `one`,
|
|
256
|
+
* it moves a single note picked at random: one share of a link split into several.
|
|
257
|
+
*/
|
|
258
|
+
sweep(token: Address, to: string, onStatus?: Status, opts?: {
|
|
259
|
+
one?: boolean;
|
|
260
|
+
}): Promise<{
|
|
261
|
+
txHash: Hex;
|
|
262
|
+
amount: bigint;
|
|
263
|
+
left: bigint;
|
|
264
|
+
}>;
|
|
251
265
|
/**
|
|
252
266
|
* Move every private balance `legacy` (a client on the earlier router, with the earlier keys) can see into this
|
|
253
267
|
* deployment, note by note, through `CloakMigrator`. Each move is one relayed transaction: the old proof withdraws to
|
package/dist/private/client.js
CHANGED
|
@@ -21,14 +21,17 @@ const json = (v) => JSON.stringify(v, (_, x) => (typeof x === "bigint" ? x.toStr
|
|
|
21
21
|
export function relayerSubmitter(url) {
|
|
22
22
|
const base = url.replace(/\/$/, "");
|
|
23
23
|
const submit = async (call) => {
|
|
24
|
-
|
|
24
|
+
// a relay normally answers in seconds; past two minutes the connection is dead, and saying so beats waiting forever
|
|
25
|
+
const res = await fetch(`${base}/relay`, { method: "POST", headers: { "content-type": "application/json" }, body: json(call), signal: AbortSignal.timeout(120_000) }).catch((e) => {
|
|
26
|
+
throw new Error(e?.name === "TimeoutError" ? "The relayer didn't answer. Check your balance in a minute before trying again." : `Couldn't reach the relayer: ${e?.message ?? e}`);
|
|
27
|
+
});
|
|
25
28
|
const out = await res.json().catch(() => ({}));
|
|
26
29
|
if (!res.ok)
|
|
27
30
|
throw new Error(out.error ?? `relay failed (${res.status})`);
|
|
28
31
|
return out.txHash;
|
|
29
32
|
};
|
|
30
33
|
submit.quote = async (kind, pool) => {
|
|
31
|
-
const res = await fetch(`${base}/quote?kind=${kind}&pool=${pool}
|
|
34
|
+
const res = await fetch(`${base}/quote?kind=${kind}&pool=${pool}`, { signal: AbortSignal.timeout(30_000) });
|
|
32
35
|
const out = await res.json().catch(() => ({}));
|
|
33
36
|
if (!res.ok)
|
|
34
37
|
throw new Error(out.error ?? `relayer quote failed (${res.status})`);
|
|
@@ -754,6 +757,67 @@ export class CloakPrivate {
|
|
|
754
757
|
});
|
|
755
758
|
return this.submit({ kind: "transact", pool, proof, extData });
|
|
756
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* Move everything this account holds of `token` to `to` in one relayed call, leaving no change behind: to a cloak:
|
|
762
|
+
* address (stays private) or out to a 0x address (a withdrawal; HYPE arrives as native HYPE, minus the withdrawal
|
|
763
|
+
* fee). The relayer's fee comes out of the amount. Spends the two largest notes of one tree epoch, so an account
|
|
764
|
+
* with more notes than that needs another call. Claim links use this, so a claimed link is left empty. With `one`,
|
|
765
|
+
* it moves a single note picked at random: one share of a link split into several.
|
|
766
|
+
*/
|
|
767
|
+
async sweep(token, to, onStatus = () => { }, opts = {}) {
|
|
768
|
+
const pool = this.market(token).pool;
|
|
769
|
+
const isHype = pool.toLowerCase() === this.hypeMarket.pool.toLowerCase();
|
|
770
|
+
const out = !to.startsWith("cloak:");
|
|
771
|
+
if (out && !/^0x[0-9a-fA-F]{40}$/.test(to))
|
|
772
|
+
throw new Error("send to a cloak: address or a 0x address");
|
|
773
|
+
const kind = out && isHype ? "unshieldHype" : "transact";
|
|
774
|
+
const [live, q] = await Promise.all([this.sync(pool), this.relayFee(kind, pool)]);
|
|
775
|
+
if (!live.length)
|
|
776
|
+
throw new Error("nothing to move: this balance is empty");
|
|
777
|
+
const sorted = [...live].sort((a, b) => (a.amount > b.amount ? -1 : a.amount < b.amount ? 1 : 0));
|
|
778
|
+
// `one`: take a single note, picked at random, so people claiming shares of one link at once rarely collide
|
|
779
|
+
const worth = sorted.filter((n) => n.amount > q.fee);
|
|
780
|
+
const inputs = opts.one
|
|
781
|
+
? worth.length ? [worth[Math.floor(Math.random() * worth.length)]] : []
|
|
782
|
+
: sorted.filter((n) => (n.epoch ?? 0) === (sorted[0].epoch ?? 0)).slice(0, 2);
|
|
783
|
+
if (!inputs.length)
|
|
784
|
+
throw new Error("this balance is smaller than the relayer's fee");
|
|
785
|
+
const sum = inputs.reduce((a, x) => a + x.amount, 0n);
|
|
786
|
+
const amount = sum - q.fee;
|
|
787
|
+
if (amount <= 0n)
|
|
788
|
+
throw new Error("this balance is smaller than the relayer's fee");
|
|
789
|
+
onStatus("Generating zero-knowledge proof on this device…");
|
|
790
|
+
let built;
|
|
791
|
+
if (out) {
|
|
792
|
+
built = await buildTransaction({
|
|
793
|
+
tree: await this.treeFor(pool, inputs),
|
|
794
|
+
inputs,
|
|
795
|
+
outputs: [],
|
|
796
|
+
extAmount: -amount,
|
|
797
|
+
fee: q.fee,
|
|
798
|
+
relayer: q.relayer,
|
|
799
|
+
recipient: isHype ? this.router : to,
|
|
800
|
+
...(isHype ? { data: encodeAbiParameters([{ type: "address" }], [to]) } : {}),
|
|
801
|
+
...(await this.dummy()),
|
|
802
|
+
prover: await this.prover(),
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
const rcpt = parseCloakAddress(to);
|
|
807
|
+
built = await buildTransaction({
|
|
808
|
+
tree: await this.treeFor(pool, inputs),
|
|
809
|
+
inputs,
|
|
810
|
+
outputs: [{ amount, pubKey: rcpt.pubKey, memo: (b) => encryptFullMemo(rcpt.encPub, amount, b, MAIN_KEY) }],
|
|
811
|
+
fee: q.fee,
|
|
812
|
+
relayer: q.relayer,
|
|
813
|
+
...(await this.dummy()),
|
|
814
|
+
prover: await this.prover(),
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
onStatus(out ? "Relaying withdrawal…" : "Relaying private transfer…");
|
|
818
|
+
const txHash = await this.submit({ kind, pool, proof: built.proof, extData: built.extData });
|
|
819
|
+
return { txHash, amount, left: live.reduce((a, x) => a + x.amount, 0n) - sum };
|
|
820
|
+
}
|
|
757
821
|
// ───────────── moving a balance from an earlier deployment ─────────────
|
|
758
822
|
/**
|
|
759
823
|
* Move every private balance `legacy` (a client on the earlier router, with the earlier keys) can see into this
|
package/dist/private/index.d.ts
CHANGED
package/dist/private/index.js
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type Hex } from "viem";
|
|
2
|
+
import { CloakKeys } from "./crypto.js";
|
|
3
|
+
export declare const LINK_ORIGIN = "https://cloak.boblabs.tech";
|
|
4
|
+
export declare const LINK_MESSAGE_MAX = 140;
|
|
5
|
+
export declare const LINK_NAME_MAX = 40;
|
|
6
|
+
export declare const LINK_HINT_MAX = 120;
|
|
7
|
+
/** Most shares one link can be split into. Each share is its own private send, so more means a longer wait to make it. */
|
|
8
|
+
export declare const LINK_SHARES_MAX = 20;
|
|
9
|
+
/** Tokens a link can carry today. */
|
|
10
|
+
export declare const LINK_TOKENS: readonly ["HYPE", "USDC"];
|
|
11
|
+
export type LinkToken = (typeof LINK_TOKENS)[number];
|
|
12
|
+
/** Unicode-safe trim to `max` characters, with control characters removed. */
|
|
13
|
+
export declare function cleanLinkText(s: string | null | undefined, max: number): string;
|
|
14
|
+
/** A passcode as both sides type it: case, outer spaces and repeated spaces don't matter. */
|
|
15
|
+
export declare const normalizePasscode: (code: string) => string;
|
|
16
|
+
/** A fresh link secret (32 random bytes). */
|
|
17
|
+
export declare const newLinkSecret: () => Uint8Array;
|
|
18
|
+
/**
|
|
19
|
+
* The private account a claim link opens. Without a passcode it comes from the secret alone. With one, the seed is
|
|
20
|
+
* scrypt(passcode, secret), so a copy of the link is useless without the code, and guessing codes is slow.
|
|
21
|
+
*/
|
|
22
|
+
export declare function linkKeys(secret: Uint8Array, passcode?: string): Promise<CloakKeys>;
|
|
23
|
+
/** The cloak: address a link's money is sent to. */
|
|
24
|
+
export declare function linkAddress(keys: CloakKeys): Promise<string>;
|
|
25
|
+
export interface ClaimLink {
|
|
26
|
+
secret: Uint8Array;
|
|
27
|
+
/** Block the link was funded in. Shown to the claimer; scanning still starts at the vaults' first block, because
|
|
28
|
+
* a note's Merkle proof needs every leaf before it. */
|
|
29
|
+
block: bigint;
|
|
30
|
+
/** What the sender put in (a display hint; the claim page reads the real balance). */
|
|
31
|
+
token?: LinkToken;
|
|
32
|
+
amount?: string;
|
|
33
|
+
/** A message from the sender. */
|
|
34
|
+
message?: string;
|
|
35
|
+
/** The sender's name, as they chose to give it. */
|
|
36
|
+
from?: string;
|
|
37
|
+
/** Needs a passcode. */
|
|
38
|
+
locked?: boolean;
|
|
39
|
+
/** A hint or riddle whose answer is the passcode. */
|
|
40
|
+
hint?: string;
|
|
41
|
+
/** Split into this many equal shares: each person who opens the link claims one (a red envelope). */
|
|
42
|
+
shares?: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function buildClaimLink(l: ClaimLink, origin?: string): string;
|
|
45
|
+
/** Reads a claim link (the whole URL or just its fragment). Throws on anything that isn't one. */
|
|
46
|
+
export declare function parseClaimLink(link: string): ClaimLink;
|
|
47
|
+
export interface PaymentRequest {
|
|
48
|
+
/** A cloak: address (paid privately) or a 0x address (paid from the payer's private balance). */
|
|
49
|
+
to: string;
|
|
50
|
+
token: LinkToken;
|
|
51
|
+
/** Whole units as a decimal ("25", "0.5"); leave out to let the payer choose. */
|
|
52
|
+
amount?: string;
|
|
53
|
+
/** What it's for: an invoice number, an order. Only the payer sees it. */
|
|
54
|
+
note?: string;
|
|
55
|
+
/** Who is asking. */
|
|
56
|
+
name?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Checks an amount and returns it as a plain decimal, or throws a plain-language error. Empty means "payer chooses". */
|
|
59
|
+
export declare function normalizeLinkAmount(input: string, decimals: number): string;
|
|
60
|
+
/** Throws unless `to` is a cloak: address or a 0x address. */
|
|
61
|
+
export declare function checkPayee(to: string): "cloak" | "wallet";
|
|
62
|
+
export declare function buildRequestLink(r: PaymentRequest, origin?: string): string;
|
|
63
|
+
/** Reads a request link (fragment first, then the query string, so hand-made `?to=…` links work too). */
|
|
64
|
+
export declare function parseRequestLink(link: string): PaymentRequest;
|
|
65
|
+
export type { Hex };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment links: money and requests that travel as a URL.
|
|
3
|
+
*
|
|
4
|
+
* claim link https://cloak.boblabs.tech/claim#k=<secret>&b=<block>&t=HYPE&m=Happy+birthday
|
|
5
|
+
* The link holds a whole throwaway private account. The sender funds it with a private send, and
|
|
6
|
+
* whoever opens the link moves that money into their own private balance, or out to any wallet. The
|
|
7
|
+
* relayer submits the claim and takes its fee from the link, so the claimer needs no gas and no
|
|
8
|
+
* Cloak account. Until someone claims it, the sender can take it back the same way.
|
|
9
|
+
* With a passcode (`p=1`), the account's seed also needs the code, sent another way or posed as a
|
|
10
|
+
* riddle (`q=`): a link posted in public that only the right person can open.
|
|
11
|
+
* Split into shares (`n=`), it is a red envelope: several equal notes, and each person who opens the
|
|
12
|
+
* link claims one, first come first served.
|
|
13
|
+
*
|
|
14
|
+
* request link https://cloak.boblabs.tech/pay#to=cloak:…&amount=25&token=USDC¬e=Invoice+42&name=Robin
|
|
15
|
+
* Asks someone to pay you. `to` is a cloak: address (a private payment) or a 0x address (paid from
|
|
16
|
+
* the payer's private balance, so the payer's wallet never shows). Leave out the amount for a tip
|
|
17
|
+
* jar. Nothing about a request is on chain.
|
|
18
|
+
*
|
|
19
|
+
* Everything sits in the URL fragment (after #), which browsers never send to a server or put in a referrer, so a
|
|
20
|
+
* link stays between the people who share it.
|
|
21
|
+
*/
|
|
22
|
+
import { scryptAsync } from "@noble/hashes/scrypt";
|
|
23
|
+
import { randomBytes } from "@noble/hashes/utils";
|
|
24
|
+
import { concat, isAddress, keccak256, toBytes } from "viem";
|
|
25
|
+
import { CloakKeys, formatCloakAddress, parseCloakAddress } from "./crypto.js";
|
|
26
|
+
export const LINK_ORIGIN = "https://cloak.boblabs.tech";
|
|
27
|
+
export const LINK_MESSAGE_MAX = 140;
|
|
28
|
+
export const LINK_NAME_MAX = 40;
|
|
29
|
+
export const LINK_HINT_MAX = 120;
|
|
30
|
+
/** Most shares one link can be split into. Each share is its own private send, so more means a longer wait to make it. */
|
|
31
|
+
export const LINK_SHARES_MAX = 20;
|
|
32
|
+
/** Tokens a link can carry today. */
|
|
33
|
+
export const LINK_TOKENS = ["HYPE", "USDC"];
|
|
34
|
+
const b64url = (b) => btoa(String.fromCharCode(...b)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
35
|
+
const unb64url = (s) => {
|
|
36
|
+
const t = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
37
|
+
return Uint8Array.from(atob(t + "=".repeat((4 - (t.length % 4)) % 4)), (c) => c.charCodeAt(0));
|
|
38
|
+
};
|
|
39
|
+
/** Unicode-safe trim to `max` characters, with control characters removed. */
|
|
40
|
+
export function cleanLinkText(s, max) {
|
|
41
|
+
// eslint-disable-next-line no-control-regex
|
|
42
|
+
return Array.from((s ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim()).slice(0, max).join("");
|
|
43
|
+
}
|
|
44
|
+
/** A passcode as both sides type it: case, outer spaces and repeated spaces don't matter. */
|
|
45
|
+
export const normalizePasscode = (code) => code.normalize("NFKC").trim().toLowerCase().replace(/\s+/g, " ");
|
|
46
|
+
/** A fresh link secret (32 random bytes). */
|
|
47
|
+
export const newLinkSecret = () => randomBytes(32);
|
|
48
|
+
/**
|
|
49
|
+
* The private account a claim link opens. Without a passcode it comes from the secret alone. With one, the seed is
|
|
50
|
+
* scrypt(passcode, secret), so a copy of the link is useless without the code, and guessing codes is slow.
|
|
51
|
+
*/
|
|
52
|
+
export async function linkKeys(secret, passcode) {
|
|
53
|
+
if (secret.length !== 32)
|
|
54
|
+
throw new Error("bad link secret");
|
|
55
|
+
if (passcode === undefined)
|
|
56
|
+
return CloakKeys.fromSeed(toBytes(keccak256(concat([toBytes("cloak-link-v1"), secret]))));
|
|
57
|
+
const code = normalizePasscode(passcode);
|
|
58
|
+
if (!code)
|
|
59
|
+
throw new Error("enter the passcode");
|
|
60
|
+
const seed = await scryptAsync(toBytes(code), concat([toBytes("cloak-link-lock-v1"), secret]), { N: 2 ** 15, r: 8, p: 1, dkLen: 32 });
|
|
61
|
+
return CloakKeys.fromSeed(seed);
|
|
62
|
+
}
|
|
63
|
+
/** The cloak: address a link's money is sent to. */
|
|
64
|
+
export async function linkAddress(keys) {
|
|
65
|
+
return formatCloakAddress((await keys.main()).pubKey, keys.encPub);
|
|
66
|
+
}
|
|
67
|
+
export function buildClaimLink(l, origin = LINK_ORIGIN) {
|
|
68
|
+
const p = new URLSearchParams();
|
|
69
|
+
p.set("k", b64url(l.secret));
|
|
70
|
+
p.set("b", l.block.toString());
|
|
71
|
+
if (l.token)
|
|
72
|
+
p.set("t", l.token);
|
|
73
|
+
if (l.amount)
|
|
74
|
+
p.set("a", l.amount);
|
|
75
|
+
const m = cleanLinkText(l.message, LINK_MESSAGE_MAX);
|
|
76
|
+
if (m)
|
|
77
|
+
p.set("m", m);
|
|
78
|
+
const f = cleanLinkText(l.from, LINK_NAME_MAX);
|
|
79
|
+
if (f)
|
|
80
|
+
p.set("f", f);
|
|
81
|
+
if (l.locked)
|
|
82
|
+
p.set("p", "1");
|
|
83
|
+
const q = cleanLinkText(l.hint, LINK_HINT_MAX);
|
|
84
|
+
if (l.locked && q)
|
|
85
|
+
p.set("q", q);
|
|
86
|
+
if (l.shares && l.shares > 1)
|
|
87
|
+
p.set("n", String(Math.min(Math.floor(l.shares), LINK_SHARES_MAX)));
|
|
88
|
+
return `${origin.replace(/\/+$/, "")}/claim#${p.toString()}`;
|
|
89
|
+
}
|
|
90
|
+
/** Reads a claim link (the whole URL or just its fragment). Throws on anything that isn't one. */
|
|
91
|
+
export function parseClaimLink(link) {
|
|
92
|
+
const frag = link.includes("#") ? link.slice(link.indexOf("#") + 1) : link;
|
|
93
|
+
const p = new URLSearchParams(frag);
|
|
94
|
+
const k = p.get("k");
|
|
95
|
+
if (!k)
|
|
96
|
+
throw new Error("This isn't a Cloak claim link.");
|
|
97
|
+
let secret;
|
|
98
|
+
try {
|
|
99
|
+
secret = unb64url(k);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new Error("This claim link is damaged. Ask the sender to copy it again.");
|
|
103
|
+
}
|
|
104
|
+
if (secret.length !== 32)
|
|
105
|
+
throw new Error("This claim link is cut short. Ask the sender to copy it again.");
|
|
106
|
+
const b = p.get("b") ?? "0";
|
|
107
|
+
const t = p.get("t");
|
|
108
|
+
const a = p.get("a");
|
|
109
|
+
return {
|
|
110
|
+
secret,
|
|
111
|
+
block: /^\d{1,12}$/.test(b) ? BigInt(b) : 0n,
|
|
112
|
+
token: LINK_TOKENS.includes(t ?? "") ? t : undefined,
|
|
113
|
+
amount: a && /^\d{1,15}(\.\d{1,18})?$/.test(a) ? a : undefined,
|
|
114
|
+
message: cleanLinkText(p.get("m"), LINK_MESSAGE_MAX) || undefined,
|
|
115
|
+
from: cleanLinkText(p.get("f"), LINK_NAME_MAX) || undefined,
|
|
116
|
+
locked: p.get("p") === "1",
|
|
117
|
+
hint: cleanLinkText(p.get("q"), LINK_HINT_MAX) || undefined,
|
|
118
|
+
shares: /^\d{1,2}$/.test(p.get("n") ?? "") && Number(p.get("n")) > 1 ? Math.min(Number(p.get("n")), LINK_SHARES_MAX) : undefined,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Checks an amount and returns it as a plain decimal, or throws a plain-language error. Empty means "payer chooses". */
|
|
122
|
+
export function normalizeLinkAmount(input, decimals) {
|
|
123
|
+
const s = input.trim().replace(/,/g, "");
|
|
124
|
+
if (!s)
|
|
125
|
+
return "";
|
|
126
|
+
if (!/^(\d+\.?\d*|\.\d+)$/.test(s))
|
|
127
|
+
throw new Error("Use numbers and one dot for the amount, like 25 or 0.5.");
|
|
128
|
+
const [i = "", f = ""] = s.split(".");
|
|
129
|
+
if (f.length > decimals)
|
|
130
|
+
throw new Error(`Use at most ${decimals} decimal places.`);
|
|
131
|
+
const int = i.replace(/^0+(?=\d)/, "") || "0";
|
|
132
|
+
const frac = f.replace(/0+$/, "");
|
|
133
|
+
if (int.length > 15)
|
|
134
|
+
throw new Error("That amount is too large.");
|
|
135
|
+
if (/^0*$/.test(int) && !frac)
|
|
136
|
+
throw new Error("Use an amount above zero, or leave it empty.");
|
|
137
|
+
return frac ? `${int}.${frac}` : int;
|
|
138
|
+
}
|
|
139
|
+
/** Throws unless `to` is a cloak: address or a 0x address. */
|
|
140
|
+
export function checkPayee(to) {
|
|
141
|
+
const t = to.trim();
|
|
142
|
+
if (t.startsWith("cloak:")) {
|
|
143
|
+
parseCloakAddress(t);
|
|
144
|
+
return "cloak";
|
|
145
|
+
}
|
|
146
|
+
if (isAddress(t))
|
|
147
|
+
return "wallet";
|
|
148
|
+
throw new Error("Pay to a cloak: address or a 0x wallet address.");
|
|
149
|
+
}
|
|
150
|
+
export function buildRequestLink(r, origin = LINK_ORIGIN) {
|
|
151
|
+
checkPayee(r.to);
|
|
152
|
+
if (!LINK_TOKENS.includes(r.token))
|
|
153
|
+
throw new Error(`Requests take ${LINK_TOKENS.join(" or ")}.`);
|
|
154
|
+
const p = new URLSearchParams();
|
|
155
|
+
p.set("to", r.to.trim());
|
|
156
|
+
if (r.amount)
|
|
157
|
+
p.set("amount", normalizeLinkAmount(r.amount, r.token === "USDC" ? 6 : 18));
|
|
158
|
+
p.set("token", r.token);
|
|
159
|
+
const note = cleanLinkText(r.note, LINK_MESSAGE_MAX);
|
|
160
|
+
if (note)
|
|
161
|
+
p.set("note", note);
|
|
162
|
+
const name = cleanLinkText(r.name, LINK_NAME_MAX);
|
|
163
|
+
if (name)
|
|
164
|
+
p.set("name", name);
|
|
165
|
+
return `${origin.replace(/\/+$/, "")}/pay#${p.toString()}`;
|
|
166
|
+
}
|
|
167
|
+
/** Reads a request link (fragment first, then the query string, so hand-made `?to=…` links work too). */
|
|
168
|
+
export function parseRequestLink(link) {
|
|
169
|
+
const url = new URL(link, LINK_ORIGIN);
|
|
170
|
+
const p = new URLSearchParams(url.search);
|
|
171
|
+
const frag = url.hash.replace(/^#/, "");
|
|
172
|
+
for (const [k, v] of new URLSearchParams(frag))
|
|
173
|
+
p.set(k, v);
|
|
174
|
+
const to = (p.get("to") ?? "").trim();
|
|
175
|
+
checkPayee(to);
|
|
176
|
+
const token = ((p.get("token") ?? "HYPE").toUpperCase() === "USDC" ? "USDC" : "HYPE");
|
|
177
|
+
const raw = p.get("amount") ?? "";
|
|
178
|
+
let amount;
|
|
179
|
+
try {
|
|
180
|
+
amount = normalizeLinkAmount(raw, token === "USDC" ? 6 : 18) || undefined;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
amount = undefined;
|
|
184
|
+
}
|
|
185
|
+
return { to, token, amount, note: cleanLinkText(p.get("note"), LINK_MESSAGE_MAX) || undefined, name: cleanLinkText(p.get("name"), LINK_NAME_MAX) || undefined };
|
|
186
|
+
}
|
package/dist/private/shared.js
CHANGED
|
@@ -36,14 +36,16 @@ const json = (v) => JSON.stringify(v, (_, x) => (typeof x === "bigint" ? x.toStr
|
|
|
36
36
|
export function sharedRelayerSubmitter(url) {
|
|
37
37
|
const base = url.replace(/\/$/, "");
|
|
38
38
|
const submit = async (call) => {
|
|
39
|
-
const res = await fetch(`${base}/relay-shared`, { method: "POST", headers: { "content-type": "application/json" }, body: json(call) })
|
|
39
|
+
const res = await fetch(`${base}/relay-shared`, { method: "POST", headers: { "content-type": "application/json" }, body: json(call), signal: AbortSignal.timeout(120_000) }).catch((e) => {
|
|
40
|
+
throw new Error(e?.name === "TimeoutError" ? "The relayer didn't answer. Check your balance in a minute before trying again." : `Couldn't reach the relayer: ${e?.message ?? e}`);
|
|
41
|
+
});
|
|
40
42
|
const out = await res.json().catch(() => ({}));
|
|
41
43
|
if (!res.ok)
|
|
42
44
|
throw new Error(out.error ?? `relay failed (${res.status})`);
|
|
43
45
|
return out.txHash;
|
|
44
46
|
};
|
|
45
47
|
submit.quote = async (kind, token) => {
|
|
46
|
-
const res = await fetch(`${base}/quote-shared?kind=${kind}&token=${token}
|
|
48
|
+
const res = await fetch(`${base}/quote-shared?kind=${kind}&token=${token}`, { signal: AbortSignal.timeout(30_000) });
|
|
47
49
|
const out = await res.json().catch(() => ({}));
|
|
48
50
|
if (!res.ok)
|
|
49
51
|
throw new Error(out.error ?? `relayer quote failed (${res.status})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloakely/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|