@unicitylabs/uniclaw 0.1.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,42 @@
1
+ /** Agent tool: uniclaw_get_transaction_history — view transaction history. */
2
+
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getSphere } from "../sphere.js";
5
+ import { getCoinDecimals, toHumanReadable } from "../assets.js";
6
+
7
+ export const getTransactionHistoryTool = {
8
+ name: "uniclaw_get_transaction_history",
9
+ description:
10
+ "Get recent transaction history for the wallet. Returns the most recent transactions first.",
11
+ parameters: Type.Object({
12
+ limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default 20)", minimum: 1 })),
13
+ }),
14
+ async execute(_toolCallId: string, params: { limit?: number }) {
15
+ const sphere = getSphere();
16
+ const history = sphere.payments.getHistory();
17
+ const sorted = [...history].sort((a, b) => b.timestamp - a.timestamp);
18
+ const limited = sorted.slice(0, params.limit ?? 20);
19
+
20
+ if (limited.length === 0) {
21
+ return {
22
+ content: [{ type: "text" as const, text: "No transaction history." }],
23
+ };
24
+ }
25
+
26
+ const lines = limited.map((e) => {
27
+ const time = new Date(e.timestamp).toISOString();
28
+ const decimals = getCoinDecimals(e.coinId) ?? 0;
29
+ const amount = toHumanReadable(e.amount, decimals);
30
+ const peer = e.type === "SENT" && e.recipientNametag
31
+ ? ` to @${e.recipientNametag}`
32
+ : e.type === "RECEIVED" && e.senderPubkey
33
+ ? ` from ${e.senderPubkey.slice(0, 12)}…`
34
+ : "";
35
+ return `[${time}] ${e.type} ${amount} ${e.symbol}${peer}`;
36
+ });
37
+
38
+ return {
39
+ content: [{ type: "text" as const, text: lines.join("\n") }],
40
+ };
41
+ },
42
+ };
@@ -0,0 +1,82 @@
1
+ /** Agent tool: uniclaw_list_payment_requests — view incoming/outgoing payment requests. */
2
+
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getSphere } from "../sphere.js";
5
+ import { getCoinDecimals, getCoinSymbol, toHumanReadable } from "../assets.js";
6
+
7
+ export const listPaymentRequestsTool = {
8
+ name: "uniclaw_list_payment_requests",
9
+ description:
10
+ "List payment requests — incoming (others requesting payment from you), outgoing (your requests to others), or all.",
11
+ parameters: Type.Object({
12
+ direction: Type.Optional(
13
+ Type.Union([
14
+ Type.Literal("incoming"),
15
+ Type.Literal("outgoing"),
16
+ Type.Literal("all"),
17
+ ], { description: "Filter direction (default: all)" }),
18
+ ),
19
+ status: Type.Optional(
20
+ Type.Union([
21
+ Type.Literal("pending"),
22
+ Type.Literal("accepted"),
23
+ Type.Literal("rejected"),
24
+ Type.Literal("paid"),
25
+ Type.Literal("expired"),
26
+ ], { description: "Filter by status" }),
27
+ ),
28
+ }),
29
+ async execute(
30
+ _toolCallId: string,
31
+ params: {
32
+ direction?: "incoming" | "outgoing" | "all";
33
+ status?: "pending" | "accepted" | "rejected" | "paid" | "expired";
34
+ },
35
+ ) {
36
+ const sphere = getSphere();
37
+ const direction = params.direction ?? "all";
38
+ const statusFilter = params.status;
39
+ const sections: string[] = [];
40
+
41
+ if (direction === "incoming" || direction === "all") {
42
+ const incoming = sphere.payments.getPaymentRequests(
43
+ statusFilter ? { status: statusFilter } : undefined,
44
+ );
45
+ if (incoming.length > 0) {
46
+ const lines = incoming.map((r) => {
47
+ const from = r.senderNametag ? `@${r.senderNametag}` : r.senderPubkey.slice(0, 12) + "…";
48
+ const decimals = getCoinDecimals(r.coinId) ?? 0;
49
+ const amount = toHumanReadable(r.amount, decimals);
50
+ const msg = r.message ? ` — "${r.message}"` : "";
51
+ return ` ${r.requestId.slice(0, 12)}… | ${amount} ${r.symbol} from ${from} | ${r.status}${msg}`;
52
+ });
53
+ sections.push(`Incoming (${incoming.length}):\n${lines.join("\n")}`);
54
+ } else {
55
+ sections.push("Incoming: none");
56
+ }
57
+ }
58
+
59
+ if (direction === "outgoing" || direction === "all") {
60
+ const outgoing = sphere.payments.getOutgoingPaymentRequests(
61
+ statusFilter ? { status: statusFilter } : undefined,
62
+ );
63
+ if (outgoing.length > 0) {
64
+ const lines = outgoing.map((r) => {
65
+ const to = r.recipientNametag ? `@${r.recipientNametag}` : r.recipientPubkey.slice(0, 12) + "…";
66
+ const decimals = getCoinDecimals(r.coinId) ?? 0;
67
+ const amount = toHumanReadable(r.amount, decimals);
68
+ const symbol = getCoinSymbol(r.coinId);
69
+ const msg = r.message ? ` — "${r.message}"` : "";
70
+ return ` ${r.id.slice(0, 12)}… | ${amount} ${symbol} to ${to} | ${r.status}${msg}`;
71
+ });
72
+ sections.push(`Outgoing (${outgoing.length}):\n${lines.join("\n")}`);
73
+ } else {
74
+ sections.push("Outgoing: none");
75
+ }
76
+ }
77
+
78
+ return {
79
+ content: [{ type: "text" as const, text: sections.join("\n\n") }],
80
+ };
81
+ },
82
+ };
@@ -0,0 +1,51 @@
1
+ /** Agent tool: uniclaw_list_tokens — list individual tokens in the wallet. */
2
+
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getSphere } from "../sphere.js";
5
+ import { getCoinDecimals, toHumanReadable } from "../assets.js";
6
+
7
+ export const listTokensTool = {
8
+ name: "uniclaw_list_tokens",
9
+ description:
10
+ "List individual tokens in the wallet, optionally filtered by coin ID and/or status.",
11
+ parameters: Type.Object({
12
+ coinId: Type.Optional(Type.String({ description: "Filter by coin ID (e.g. 'ALPHA')" })),
13
+ status: Type.Optional(
14
+ Type.Union([
15
+ Type.Literal("pending"),
16
+ Type.Literal("confirmed"),
17
+ Type.Literal("transferring"),
18
+ Type.Literal("spent"),
19
+ Type.Literal("invalid"),
20
+ ], { description: "Filter by token status" }),
21
+ ),
22
+ }),
23
+ async execute(
24
+ _toolCallId: string,
25
+ params: { coinId?: string; status?: "pending" | "confirmed" | "transferring" | "spent" | "invalid" },
26
+ ) {
27
+ const sphere = getSphere();
28
+ const tokens = sphere.payments.getTokens({
29
+ coinId: params.coinId,
30
+ status: params.status,
31
+ });
32
+
33
+ if (tokens.length === 0) {
34
+ return {
35
+ content: [{ type: "text" as const, text: "No tokens found matching the criteria." }],
36
+ };
37
+ }
38
+
39
+ const lines = tokens.map((t) => {
40
+ const decimals = getCoinDecimals(t.coinId) ?? 0;
41
+ const amount = toHumanReadable(t.amount, decimals);
42
+ return `${t.id.slice(0, 12)}… | ${amount} ${t.symbol} | ${t.status} | ${new Date(t.createdAt).toISOString()}`;
43
+ });
44
+
45
+ return {
46
+ content: [
47
+ { type: "text" as const, text: `Found ${tokens.length} token${tokens.length !== 1 ? "s" : ""}:\n${lines.join("\n")}` },
48
+ ],
49
+ };
50
+ },
51
+ };
@@ -0,0 +1,62 @@
1
+ /** Agent tool: uniclaw_request_payment — request payment from someone. */
2
+
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getSphere } from "../sphere.js";
5
+ import { resolveCoinId, getCoinSymbol, getCoinDecimals, toSmallestUnit } from "../assets.js";
6
+ import { validateRecipient } from "../validation.js";
7
+
8
+ export const requestPaymentTool = {
9
+ name: "uniclaw_request_payment",
10
+ description:
11
+ "Send a payment request to another user, asking them to pay a specific amount.",
12
+ parameters: Type.Object({
13
+ recipient: Type.String({ description: "Nametag (e.g. @alice) or 64-char hex public key of who should pay" }),
14
+ amount: Type.Number({ description: "Amount to request (human-readable, e.g. 100 or 1.5)" }),
15
+ coin: Type.String({ description: "Coin to request by name or symbol (e.g. UCT, BTC)" }),
16
+ message: Type.Optional(Type.String({ description: "Optional message to include with the request" })),
17
+ }),
18
+ async execute(
19
+ _toolCallId: string,
20
+ params: { recipient: string; amount: number; coin: string; message?: string },
21
+ ) {
22
+ const recipient = params.recipient.trim();
23
+ validateRecipient(recipient);
24
+
25
+ if (params.amount <= 0) {
26
+ throw new Error("Amount must be greater than 0.");
27
+ }
28
+
29
+ const coinId = resolveCoinId(params.coin);
30
+ if (!coinId) {
31
+ throw new Error(`Unknown coin "${params.coin}".`);
32
+ }
33
+
34
+ const decimals = getCoinDecimals(coinId) ?? 0;
35
+ const amountSmallest = toSmallestUnit(params.amount, decimals);
36
+ const symbol = getCoinSymbol(coinId);
37
+
38
+ const sphere = getSphere();
39
+ const normalized = recipient.replace(/^@/, "");
40
+
41
+ const result = await sphere.payments.sendPaymentRequest(normalized, {
42
+ amount: amountSmallest,
43
+ coinId,
44
+ message: params.message,
45
+ });
46
+
47
+ if (!result.success) {
48
+ return {
49
+ content: [{ type: "text" as const, text: `Payment request failed: ${result.error ?? "unknown error"}` }],
50
+ };
51
+ }
52
+
53
+ return {
54
+ content: [
55
+ {
56
+ type: "text" as const,
57
+ text: `Payment request sent to ${params.recipient} for ${params.amount} ${symbol} (request id: ${result.requestId})`,
58
+ },
59
+ ],
60
+ };
61
+ },
62
+ };
@@ -0,0 +1,58 @@
1
+ /** Agent tool: uniclaw_respond_payment_request — accept, reject, or pay a payment request. */
2
+
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getSphere } from "../sphere.js";
5
+
6
+ export const respondPaymentRequestTool = {
7
+ name: "uniclaw_respond_payment_request",
8
+ description:
9
+ "Respond to an incoming payment request by paying, accepting, or rejecting it. IMPORTANT: Only pay requests when explicitly instructed by the wallet owner.",
10
+ parameters: Type.Object({
11
+ requestId: Type.String({ description: "The payment request ID to respond to" }),
12
+ action: Type.Union([
13
+ Type.Literal("pay"),
14
+ Type.Literal("accept"),
15
+ Type.Literal("reject"),
16
+ ], { description: "Action to take: pay (send tokens immediately), accept (mark as accepted), or reject" }),
17
+ memo: Type.Optional(Type.String({ description: "Optional memo (used with 'pay' action)" })),
18
+ }),
19
+ async execute(
20
+ _toolCallId: string,
21
+ params: { requestId: string; action: string; memo?: string },
22
+ ) {
23
+ const sphere = getSphere();
24
+
25
+ switch (params.action) {
26
+ case "pay": {
27
+ const result = await sphere.payments.payPaymentRequest(params.requestId, params.memo);
28
+ if (result.error) {
29
+ return {
30
+ content: [{ type: "text" as const, text: `Payment failed: ${result.error}` }],
31
+ };
32
+ }
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text" as const,
37
+ text: `Payment request ${params.requestId} paid — transfer ${result.id} (status: ${result.status})`,
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ case "accept": {
43
+ await sphere.payments.acceptPaymentRequest(params.requestId);
44
+ return {
45
+ content: [{ type: "text" as const, text: `Payment request ${params.requestId} accepted.` }],
46
+ };
47
+ }
48
+ case "reject": {
49
+ await sphere.payments.rejectPaymentRequest(params.requestId);
50
+ return {
51
+ content: [{ type: "text" as const, text: `Payment request ${params.requestId} rejected.` }],
52
+ };
53
+ }
54
+ default:
55
+ throw new Error(`Invalid action: "${params.action}". Expected "pay", "accept", or "reject".`);
56
+ }
57
+ },
58
+ };
@@ -0,0 +1,30 @@
1
+ /** Agent tool: uniclaw_send_message — send a Nostr DM via Sphere. */
2
+
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getSphere } from "../sphere.js";
5
+ import { validateRecipient } from "../validation.js";
6
+
7
+ export const sendMessageTool = {
8
+ name: "uniclaw_send_message",
9
+ description:
10
+ "Send a direct message to a Unicity/Nostr user. The recipient can be a nametag (e.g. @alice) or a hex public key.",
11
+ parameters: Type.Object({
12
+ recipient: Type.String({ description: "Nametag or public key of the recipient" }),
13
+ message: Type.String({ description: "Message text to send" }),
14
+ }),
15
+ async execute(_toolCallId: string, params: { recipient: string; message: string }) {
16
+ const recipient = params.recipient.trim();
17
+ validateRecipient(recipient);
18
+ const sphere = getSphere();
19
+ const normalized = recipient.replace(/^@/, "");
20
+ const dm = await sphere.communications.sendDM(normalized, params.message);
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text" as const,
25
+ text: `Message sent to ${params.recipient} (id: ${dm.id})`,
26
+ },
27
+ ],
28
+ };
29
+ },
30
+ };
@@ -0,0 +1,63 @@
1
+ /** Agent tool: uniclaw_send_tokens — transfer tokens to a recipient. */
2
+
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getSphere } from "../sphere.js";
5
+ import { resolveCoinId, getCoinSymbol, getCoinDecimals, toSmallestUnit } from "../assets.js";
6
+ import { validateRecipient } from "../validation.js";
7
+
8
+ export const sendTokensTool = {
9
+ name: "uniclaw_send_tokens",
10
+ description:
11
+ "Send tokens to a recipient by nametag or public key. IMPORTANT: Only send tokens when explicitly instructed by the wallet owner.",
12
+ parameters: Type.Object({
13
+ recipient: Type.String({ description: "Nametag (e.g. @alice) or 64-char hex public key" }),
14
+ amount: Type.Number({ description: "Amount to send (human-readable, e.g. 100 or 1.5)" }),
15
+ coin: Type.String({ description: "Coin to send by name or symbol (e.g. UCT, BTC)" }),
16
+ memo: Type.Optional(Type.String({ description: "Optional memo to attach to the transfer" })),
17
+ }),
18
+ async execute(
19
+ _toolCallId: string,
20
+ params: { recipient: string; amount: number; coin: string; memo?: string },
21
+ ) {
22
+ const recipient = params.recipient.trim();
23
+ validateRecipient(recipient);
24
+
25
+ if (params.amount <= 0) {
26
+ throw new Error("Amount must be greater than 0.");
27
+ }
28
+
29
+ const coinId = resolveCoinId(params.coin);
30
+ if (!coinId) {
31
+ throw new Error(`Unknown coin "${params.coin}".`);
32
+ }
33
+
34
+ const decimals = getCoinDecimals(coinId) ?? 0;
35
+ const amountSmallest = toSmallestUnit(params.amount, decimals);
36
+ const symbol = getCoinSymbol(coinId);
37
+
38
+ const sphere = getSphere();
39
+ const normalized = recipient.replace(/^@/, "");
40
+
41
+ const result = await sphere.payments.send({
42
+ recipient: normalized,
43
+ amount: amountSmallest,
44
+ coinId,
45
+ memo: params.memo,
46
+ });
47
+
48
+ if (result.error) {
49
+ return {
50
+ content: [{ type: "text" as const, text: `Transfer failed: ${result.error}` }],
51
+ };
52
+ }
53
+
54
+ return {
55
+ content: [
56
+ {
57
+ type: "text" as const,
58
+ text: `Transfer ${result.id} — ${params.amount} ${symbol} sent to ${params.recipient} (status: ${result.status})`,
59
+ },
60
+ ],
61
+ };
62
+ },
63
+ };
@@ -0,0 +1,81 @@
1
+ /** Agent tool: uniclaw_top_up — request test tokens from the faucet. */
2
+
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getSphere } from "../sphere.js";
5
+ import { resolveCoinId, getCoinSymbol, getAvailableSymbols } from "../assets.js";
6
+
7
+ const FAUCET_API_URL = process.env.UNICLAW_FAUCET_URL
8
+ ?? "https://faucet.unicity.network/api/v1/faucet/request";
9
+
10
+ export const topUpTool = {
11
+ name: "uniclaw_top_up",
12
+ description:
13
+ "Request test tokens from the Unicity faucet. This is for testnet only.",
14
+ parameters: Type.Object({
15
+ coin: Type.String({ description: "Coin to request by name or symbol (e.g. UCT, BTC, SOL)" }),
16
+ amount: Type.Number({ description: "Amount to request (can be decimal)" }),
17
+ }),
18
+ async execute(_toolCallId: string, params: { coin: string; amount: number }) {
19
+ const sphere = getSphere();
20
+ const nametag = sphere.identity?.nametag;
21
+
22
+ if (!nametag) {
23
+ throw new Error("Wallet has no nametag. A nametag is required to receive tokens from the faucet.");
24
+ }
25
+
26
+ const coinId = resolveCoinId(params.coin);
27
+ if (!coinId) {
28
+ const validCoins = getAvailableSymbols().join(", ");
29
+ throw new Error(`Unknown coin "${params.coin}". Available coins: ${validCoins}`);
30
+ }
31
+
32
+ if (params.amount <= 0) {
33
+ throw new Error("Amount must be greater than 0.");
34
+ }
35
+
36
+ const response = await fetch(FAUCET_API_URL, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify({
40
+ unicityId: nametag.replace(/^@/, ""),
41
+ coin: coinId,
42
+ amount: params.amount,
43
+ }),
44
+ });
45
+
46
+ if (!response.ok) {
47
+ const errorText = await response.text();
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text" as const,
52
+ text: `Faucet request failed: ${response.status} ${response.statusText} — ${errorText}`,
53
+ },
54
+ ],
55
+ };
56
+ }
57
+
58
+ const data = await response.json();
59
+ const displaySymbol = getCoinSymbol(coinId);
60
+
61
+ if (data.success === false) {
62
+ return {
63
+ content: [
64
+ {
65
+ type: "text" as const,
66
+ text: `Faucet request failed: ${data.message ?? "unknown error"}`,
67
+ },
68
+ ],
69
+ };
70
+ }
71
+
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text" as const,
76
+ text: `Faucet request successful: ${params.amount} ${displaySymbol} sent to @${nametag.replace(/^@/, "")}. Tokens should arrive shortly.`,
77
+ },
78
+ ],
79
+ };
80
+ },
81
+ };
@@ -0,0 +1,15 @@
1
+ /** Shared validation constants and helpers. */
2
+
3
+ /** Nametag: starts with a letter, alphanumeric + hyphens/underscores, max 32 chars. */
4
+ export const NAMETAG_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]{0,31}$/;
5
+
6
+ /** Valid recipient: nametag (with optional @) or 64-char hex public key. */
7
+ export const VALID_RECIPIENT = /^@?\w[\w-]{0,31}$|^[0-9a-fA-F]{64}$/;
8
+
9
+ export function validateRecipient(recipient: string): void {
10
+ if (!VALID_RECIPIENT.test(recipient.trim())) {
11
+ throw new Error(
12
+ `Invalid recipient format: "${recipient}". Expected a nametag or 64-char hex public key.`,
13
+ );
14
+ }
15
+ }