@paper-clip/pc 0.1.4
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/baked-config.json +18 -0
- package/dist/bin.js +18 -0
- package/dist/client.js +82 -0
- package/dist/config.js +99 -0
- package/dist/index.js +812 -0
- package/dist/privy.js +243 -0
- package/dist/settings.js +75 -0
- package/dist/storacha.js +97 -0
- package/dist/types.js +4 -0
- package/dist/ui.js +127 -0
- package/idl/paperclip_protocol.json +1060 -0
- package/package.json +43 -0
package/dist/privy.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privy Server Wallet Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides server-side Solana wallet signing via Privy's REST API.
|
|
5
|
+
* Agents never touch raw keypairs — signing happens on Privy's servers
|
|
6
|
+
* with policy controls set by the protocol team.
|
|
7
|
+
*
|
|
8
|
+
* Credentials are baked into config.ts at build time.
|
|
9
|
+
* This module is only used when WALLET_TYPE === "privy".
|
|
10
|
+
*/
|
|
11
|
+
import * as anchor from "@coral-xyz/anchor";
|
|
12
|
+
import { PublicKey, Transaction, VersionedTransaction, } from "@solana/web3.js";
|
|
13
|
+
import { NETWORK, PRIVY_APP_ID, PRIVY_APP_SECRET } from "./config.js";
|
|
14
|
+
import { loadSettings, saveSettings } from "./settings.js";
|
|
15
|
+
const PRIVY_API_BASE = "https://api.privy.io/v1";
|
|
16
|
+
const DUMMY_RECENT_BLOCKHASH = "11111111111111111111111111111111";
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// HTTP helpers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
function authHeaders() {
|
|
21
|
+
const encoded = Buffer.from(`${PRIVY_APP_ID}:${PRIVY_APP_SECRET}`).toString("base64");
|
|
22
|
+
return {
|
|
23
|
+
Authorization: `Basic ${encoded}`,
|
|
24
|
+
"privy-app-id": PRIVY_APP_ID,
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function privyFetch(path, init = {}) {
|
|
29
|
+
const url = `${PRIVY_API_BASE}${path}`;
|
|
30
|
+
const headers = {
|
|
31
|
+
...authHeaders(),
|
|
32
|
+
...(init.headers || {}),
|
|
33
|
+
};
|
|
34
|
+
const res = await fetch(url, { ...init, headers });
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const body = await res.text().catch(() => "");
|
|
37
|
+
throw new Error(`Privy API error ${res.status} on ${init.method || "GET"} ${path}: ${body}`);
|
|
38
|
+
}
|
|
39
|
+
return res.json();
|
|
40
|
+
}
|
|
41
|
+
function networkToCaip2(network) {
|
|
42
|
+
if (network === "devnet") {
|
|
43
|
+
return "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
44
|
+
}
|
|
45
|
+
if (network === "testnet") {
|
|
46
|
+
return "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z";
|
|
47
|
+
}
|
|
48
|
+
if (network === "mainnet") {
|
|
49
|
+
return "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Privy gas sponsorship is not supported for network "${network}". Use devnet/mainnet.`);
|
|
52
|
+
}
|
|
53
|
+
function extractTxHash(response) {
|
|
54
|
+
const hash = response?.data?.hash ||
|
|
55
|
+
response?.data?.signature ||
|
|
56
|
+
response?.hash ||
|
|
57
|
+
response?.signature;
|
|
58
|
+
if (typeof hash !== "string" || hash.length === 0) {
|
|
59
|
+
throw new Error(`Invalid Privy signAndSendTransaction response: ${JSON.stringify(response)}`);
|
|
60
|
+
}
|
|
61
|
+
return hash;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a new Solana server wallet via Privy.
|
|
65
|
+
* Called once during `pc init` — wallet ID is saved to ~/.paperclip/config.json.
|
|
66
|
+
*/
|
|
67
|
+
export async function createPrivyWallet() {
|
|
68
|
+
const data = await privyFetch("/wallets", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify({ chain_type: "solana" }),
|
|
71
|
+
});
|
|
72
|
+
return { id: data.id, address: data.address };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get an existing wallet's info from Privy.
|
|
76
|
+
*/
|
|
77
|
+
export async function getPrivyWalletInfo(walletId) {
|
|
78
|
+
const data = await privyFetch(`/wallets/${walletId}`);
|
|
79
|
+
return { id: data.id, address: data.address };
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// PrivyWallet — Anchor Wallet interface
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
/**
|
|
85
|
+
* Anchor-compatible Wallet that signs transactions via Privy's REST API.
|
|
86
|
+
*
|
|
87
|
+
* Duck-types the Wallet interface Anchor needs:
|
|
88
|
+
* - publicKey: PublicKey
|
|
89
|
+
* - signTransaction(tx): Promise<Transaction>
|
|
90
|
+
* - signAllTransactions(txs): Promise<Transaction[]>
|
|
91
|
+
*/
|
|
92
|
+
export class PrivyWallet {
|
|
93
|
+
publicKey;
|
|
94
|
+
walletId;
|
|
95
|
+
constructor(walletId, publicKey) {
|
|
96
|
+
this.walletId = walletId;
|
|
97
|
+
this.publicKey = publicKey;
|
|
98
|
+
}
|
|
99
|
+
get id() {
|
|
100
|
+
return this.walletId;
|
|
101
|
+
}
|
|
102
|
+
async signTransaction(tx) {
|
|
103
|
+
const serialized = Buffer.from(tx.serialize({ requireAllSignatures: false, verifySignatures: false })).toString("base64");
|
|
104
|
+
const data = await privyFetch(`/wallets/${this.walletId}/rpc`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
method: "signTransaction",
|
|
108
|
+
params: {
|
|
109
|
+
transaction: serialized,
|
|
110
|
+
encoding: "base64",
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
const signedBytes = Buffer.from(data.data?.signed_transaction || data.data?.signature || "", "base64");
|
|
115
|
+
if (tx instanceof VersionedTransaction) {
|
|
116
|
+
return VersionedTransaction.deserialize(signedBytes);
|
|
117
|
+
}
|
|
118
|
+
return Transaction.from(signedBytes);
|
|
119
|
+
}
|
|
120
|
+
async signAllTransactions(txs) {
|
|
121
|
+
// Sign sequentially — Privy doesn't have a batch sign endpoint
|
|
122
|
+
return Promise.all(txs.map((tx) => this.signTransaction(tx)));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function signAndSendSponsoredTransaction(walletId, txBase64) {
|
|
126
|
+
const caip2 = networkToCaip2(NETWORK);
|
|
127
|
+
const data = await privyFetch(`/wallets/${walletId}/rpc`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
method: "signAndSendTransaction",
|
|
131
|
+
caip2,
|
|
132
|
+
sponsor: true,
|
|
133
|
+
params: {
|
|
134
|
+
transaction: txBase64,
|
|
135
|
+
encoding: "base64",
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
return extractTxHash(data);
|
|
140
|
+
}
|
|
141
|
+
function toBase64Transaction(tx) {
|
|
142
|
+
if (tx instanceof VersionedTransaction) {
|
|
143
|
+
return Buffer.from(tx.serialize()).toString("base64");
|
|
144
|
+
}
|
|
145
|
+
// Privy signs this transaction server-side, so it may be missing wallet sig.
|
|
146
|
+
const raw = tx.serialize({
|
|
147
|
+
requireAllSignatures: false,
|
|
148
|
+
verifySignatures: false,
|
|
149
|
+
});
|
|
150
|
+
return Buffer.from(raw).toString("base64");
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Provider wrapper that routes Privy transactions through signAndSendTransaction
|
|
154
|
+
* with sponsorship enabled.
|
|
155
|
+
*/
|
|
156
|
+
export class PrivyAnchorProvider extends anchor.AnchorProvider {
|
|
157
|
+
privyWallet;
|
|
158
|
+
constructor(connection, wallet, opts = anchor.AnchorProvider.defaultOptions()) {
|
|
159
|
+
super(connection, wallet, opts);
|
|
160
|
+
this.privyWallet = wallet;
|
|
161
|
+
}
|
|
162
|
+
prepareTx(tx, signers) {
|
|
163
|
+
if (tx instanceof VersionedTransaction) {
|
|
164
|
+
if (signers && signers.length > 0) {
|
|
165
|
+
tx.sign(signers);
|
|
166
|
+
}
|
|
167
|
+
// Privy fills the real recent blockhash when processing signAndSend.
|
|
168
|
+
tx.message.recentBlockhash = DUMMY_RECENT_BLOCKHASH;
|
|
169
|
+
return tx;
|
|
170
|
+
}
|
|
171
|
+
tx.feePayer = tx.feePayer ?? this.wallet.publicKey;
|
|
172
|
+
tx.recentBlockhash = DUMMY_RECENT_BLOCKHASH;
|
|
173
|
+
if (signers && signers.length > 0) {
|
|
174
|
+
for (const signer of signers) {
|
|
175
|
+
tx.partialSign(signer);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return tx;
|
|
179
|
+
}
|
|
180
|
+
async sendAndConfirm(tx, signers = [], opts) {
|
|
181
|
+
// Keep localnet behavior unchanged (no Privy sponsorship path there).
|
|
182
|
+
if (NETWORK === "localnet") {
|
|
183
|
+
return super.sendAndConfirm(tx, signers, opts);
|
|
184
|
+
}
|
|
185
|
+
const prepared = this.prepareTx(tx, signers);
|
|
186
|
+
const txBase64 = toBase64Transaction(prepared);
|
|
187
|
+
const signature = await signAndSendSponsoredTransaction(this.privyWallet.id, txBase64);
|
|
188
|
+
const commitment = opts?.commitment ?? this.opts.commitment ?? "confirmed";
|
|
189
|
+
const status = await this.connection.confirmTransaction(signature, commitment);
|
|
190
|
+
if (status.value.err) {
|
|
191
|
+
throw new Error(`Privy sponsored transaction ${signature} failed: ${JSON.stringify(status.value.err)}`);
|
|
192
|
+
}
|
|
193
|
+
return signature;
|
|
194
|
+
}
|
|
195
|
+
async sendAll(txWithSigners, opts) {
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const item of txWithSigners) {
|
|
198
|
+
out.push(await this.sendAndConfirm(item.tx, item.signers ?? [], opts));
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Cached instance — getPrivyWalletInstance()
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
let _cachedWallet = null;
|
|
207
|
+
/**
|
|
208
|
+
* Get or create the PrivyWallet instance.
|
|
209
|
+
* Reads wallet ID from persisted settings (~/.paperclip/config.json).
|
|
210
|
+
* Throws if no wallet has been provisioned yet (agent must run `pc init` first).
|
|
211
|
+
*/
|
|
212
|
+
export async function getPrivyWalletInstance() {
|
|
213
|
+
if (_cachedWallet)
|
|
214
|
+
return _cachedWallet;
|
|
215
|
+
const settings = loadSettings();
|
|
216
|
+
const walletId = settings.privyWalletId;
|
|
217
|
+
const walletAddress = settings.privyWalletAddress;
|
|
218
|
+
if (!walletId || !walletAddress) {
|
|
219
|
+
throw new Error("No Privy wallet found. Run `pc init` to create one.");
|
|
220
|
+
}
|
|
221
|
+
_cachedWallet = new PrivyWallet(walletId, new PublicKey(walletAddress));
|
|
222
|
+
return _cachedWallet;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Provision a new wallet and save to settings.
|
|
226
|
+
* Called during `pc init` when using Privy mode.
|
|
227
|
+
*/
|
|
228
|
+
export async function provisionPrivyWallet() {
|
|
229
|
+
const settings = loadSettings();
|
|
230
|
+
// Already provisioned?
|
|
231
|
+
if (settings.privyWalletId && settings.privyWalletAddress) {
|
|
232
|
+
_cachedWallet = new PrivyWallet(settings.privyWalletId, new PublicKey(settings.privyWalletAddress));
|
|
233
|
+
return _cachedWallet;
|
|
234
|
+
}
|
|
235
|
+
// Create new wallet
|
|
236
|
+
const info = await createPrivyWallet();
|
|
237
|
+
// Persist
|
|
238
|
+
settings.privyWalletId = info.id;
|
|
239
|
+
settings.privyWalletAddress = info.address;
|
|
240
|
+
saveSettings(settings);
|
|
241
|
+
_cachedWallet = new PrivyWallet(info.id, new PublicKey(info.address));
|
|
242
|
+
return _cachedWallet;
|
|
243
|
+
}
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent CLI settings — stored at ~/.paperclip/config.json
|
|
3
|
+
*
|
|
4
|
+
* Supports agent mode (JSON, no spinners — default) and
|
|
5
|
+
* human mode (emoji, colors, spinners).
|
|
6
|
+
*/
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import os from "os";
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// PATHS
|
|
12
|
+
// =============================================================================
|
|
13
|
+
const CONFIG_DIR = path.join(os.homedir(), ".paperclip");
|
|
14
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// DEFAULTS
|
|
17
|
+
// =============================================================================
|
|
18
|
+
const DEFAULTS = {
|
|
19
|
+
mode: "agent",
|
|
20
|
+
network: "devnet",
|
|
21
|
+
};
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// READ / WRITE
|
|
24
|
+
// =============================================================================
|
|
25
|
+
export function loadSettings() {
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
28
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
29
|
+
return { ...DEFAULTS, ...raw };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Corrupted file — return defaults
|
|
34
|
+
}
|
|
35
|
+
return { ...DEFAULTS };
|
|
36
|
+
}
|
|
37
|
+
export function saveSettings(settings) {
|
|
38
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
39
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(settings, null, 2) + "\n");
|
|
42
|
+
}
|
|
43
|
+
export function getMode() {
|
|
44
|
+
return loadSettings().mode;
|
|
45
|
+
}
|
|
46
|
+
export function setMode(mode) {
|
|
47
|
+
const settings = loadSettings();
|
|
48
|
+
settings.mode = mode;
|
|
49
|
+
saveSettings(settings);
|
|
50
|
+
}
|
|
51
|
+
export function getNetwork() {
|
|
52
|
+
return loadSettings().network;
|
|
53
|
+
}
|
|
54
|
+
export function getConfiguredNetwork() {
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
60
|
+
return raw.network === "devnet" || raw.network === "localnet"
|
|
61
|
+
? raw.network
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function setNetwork(network) {
|
|
69
|
+
const settings = loadSettings();
|
|
70
|
+
settings.network = network;
|
|
71
|
+
saveSettings(settings);
|
|
72
|
+
}
|
|
73
|
+
export function configPath() {
|
|
74
|
+
return CONFIG_FILE;
|
|
75
|
+
}
|
package/dist/storacha.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { create } from "@storacha/client";
|
|
2
|
+
import { StoreMemory } from "@storacha/client/stores/memory";
|
|
3
|
+
import * as Proof from "@storacha/client/proof";
|
|
4
|
+
import { Signer } from "@storacha/client/principal/ed25519";
|
|
5
|
+
import { File } from "@web-std/file";
|
|
6
|
+
import { STORACHA_GATEWAY_URL, STORACHA_AGENT_KEY, W3UP_DATA_SPACE_DID, W3UP_DATA_SPACE_PROOF, W3UP_MESSAGES_SPACE_DID, W3UP_MESSAGES_SPACE_PROOF, W3UP_TASKS_SPACE_DID, W3UP_TASKS_SPACE_PROOF, } from "./config.js";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
/**
|
|
10
|
+
* Create a Storacha client with the correct principal key.
|
|
11
|
+
*
|
|
12
|
+
* If STORACHA_AGENT_KEY is set, uses that as the agent identity.
|
|
13
|
+
* Otherwise falls back to auto-generated key (may fail if proof
|
|
14
|
+
* audience doesn't match).
|
|
15
|
+
*/
|
|
16
|
+
async function createClient() {
|
|
17
|
+
if (STORACHA_AGENT_KEY) {
|
|
18
|
+
const principal = Signer.parse(STORACHA_AGENT_KEY);
|
|
19
|
+
const store = new StoreMemory();
|
|
20
|
+
return create({ principal, store });
|
|
21
|
+
}
|
|
22
|
+
return create();
|
|
23
|
+
}
|
|
24
|
+
function resolveSpace(scope) {
|
|
25
|
+
if (scope === "data") {
|
|
26
|
+
return { did: W3UP_DATA_SPACE_DID, proof: W3UP_DATA_SPACE_PROOF };
|
|
27
|
+
}
|
|
28
|
+
if (scope === "tasks") {
|
|
29
|
+
return { did: W3UP_TASKS_SPACE_DID, proof: W3UP_TASKS_SPACE_PROOF };
|
|
30
|
+
}
|
|
31
|
+
return { did: W3UP_MESSAGES_SPACE_DID, proof: W3UP_MESSAGES_SPACE_PROOF };
|
|
32
|
+
}
|
|
33
|
+
async function ensureSpace(client, scope) {
|
|
34
|
+
const { did, proof } = resolveSpace(scope);
|
|
35
|
+
const proofStr = proof;
|
|
36
|
+
if (!proofStr) {
|
|
37
|
+
throw new Error(`No Storacha delegation proof configured for "${scope}" uploads. Set W3UP_${scope.toUpperCase()}_SPACE_PROOF.`);
|
|
38
|
+
}
|
|
39
|
+
const delegation = await Proof.parse(proofStr);
|
|
40
|
+
const space = await client.addSpace(delegation);
|
|
41
|
+
const targetDid = (did || space.did());
|
|
42
|
+
const currentDid = currentSpaceDid(client);
|
|
43
|
+
if (currentDid !== targetDid) {
|
|
44
|
+
await client.setCurrentSpace(targetDid);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function normalizeGateway(base) {
|
|
48
|
+
return base.endsWith("/") ? base : `${base}/`;
|
|
49
|
+
}
|
|
50
|
+
function currentSpaceDid(client) {
|
|
51
|
+
const current = client.currentSpace();
|
|
52
|
+
if (!current) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (typeof current === "string") {
|
|
56
|
+
return current;
|
|
57
|
+
}
|
|
58
|
+
if (typeof current.did === "function") {
|
|
59
|
+
return current.did();
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
export async function uploadJson(data, scope = "data") {
|
|
64
|
+
if (process.env.PAPERCLIP_STORACHA_MOCK === "1") {
|
|
65
|
+
const payload = JSON.stringify(data);
|
|
66
|
+
const hash = crypto.createHash("sha256").update(payload).digest("hex");
|
|
67
|
+
return `mock-${hash.slice(0, 16)}`;
|
|
68
|
+
}
|
|
69
|
+
const client = await createClient();
|
|
70
|
+
await ensureSpace(client, scope);
|
|
71
|
+
const payload = JSON.stringify(data, null, 2);
|
|
72
|
+
const file = new File([payload], "proof.json", {
|
|
73
|
+
type: "application/json",
|
|
74
|
+
});
|
|
75
|
+
const cid = await client.uploadFile(file);
|
|
76
|
+
return typeof cid === "string" ? cid : cid.toString();
|
|
77
|
+
}
|
|
78
|
+
export async function fetchJson(cid) {
|
|
79
|
+
if (process.env.PAPERCLIP_STORACHA_MOCK === "1") {
|
|
80
|
+
const inline = process.env.PAPERCLIP_MOCK_TASK_JSON;
|
|
81
|
+
if (inline) {
|
|
82
|
+
return JSON.parse(inline);
|
|
83
|
+
}
|
|
84
|
+
const path = process.env.PAPERCLIP_MOCK_TASK_JSON_PATH;
|
|
85
|
+
if (path && fs.existsSync(path)) {
|
|
86
|
+
return JSON.parse(fs.readFileSync(path, "utf8"));
|
|
87
|
+
}
|
|
88
|
+
return { mock: true, cid };
|
|
89
|
+
}
|
|
90
|
+
const base = normalizeGateway(STORACHA_GATEWAY_URL);
|
|
91
|
+
const url = `${base}${cid}`;
|
|
92
|
+
const res = await fetch(url);
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
throw new Error(`Failed to fetch CID ${cid}: ${res.status}`);
|
|
95
|
+
}
|
|
96
|
+
return res.json();
|
|
97
|
+
}
|
package/dist/types.js
ADDED
package/dist/ui.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI helpers — spinners, colored output, error parsing, formatted tables.
|
|
3
|
+
*/
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// BRANDING
|
|
8
|
+
// =============================================================================
|
|
9
|
+
const BRAND = chalk.bold.magenta("📎 Paperclip");
|
|
10
|
+
const DIVIDER = chalk.dim("━".repeat(50));
|
|
11
|
+
export function banner() {
|
|
12
|
+
console.log();
|
|
13
|
+
console.log(DIVIDER);
|
|
14
|
+
console.log(` ${BRAND} ${chalk.dim("Protocol CLI")}`);
|
|
15
|
+
console.log(DIVIDER);
|
|
16
|
+
}
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// SPINNERS
|
|
19
|
+
// =============================================================================
|
|
20
|
+
export function spin(text) {
|
|
21
|
+
return ora({ text, color: "magenta" }).start();
|
|
22
|
+
}
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// OUTPUT HELPERS
|
|
25
|
+
// =============================================================================
|
|
26
|
+
export function success(msg) {
|
|
27
|
+
console.log(chalk.green(` ✅ ${msg}`));
|
|
28
|
+
}
|
|
29
|
+
export function info(label, value) {
|
|
30
|
+
console.log(` ${chalk.dim(label)} ${chalk.white(String(value))}`);
|
|
31
|
+
}
|
|
32
|
+
export function warn(msg) {
|
|
33
|
+
console.log(chalk.yellow(` ⚠️ ${msg}`));
|
|
34
|
+
}
|
|
35
|
+
export function fail(msg) {
|
|
36
|
+
console.log(chalk.red(` ❌ ${msg}`));
|
|
37
|
+
}
|
|
38
|
+
export function heading(title) {
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(` ${chalk.bold(title)}`);
|
|
41
|
+
console.log(` ${chalk.dim("─".repeat(40))}`);
|
|
42
|
+
}
|
|
43
|
+
export function blank() {
|
|
44
|
+
console.log();
|
|
45
|
+
}
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// TABLE
|
|
48
|
+
// =============================================================================
|
|
49
|
+
export function table(headers, rows) {
|
|
50
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] ?? "").length)));
|
|
51
|
+
const sep = widths.map((w) => "─".repeat(w + 2)).join("┼");
|
|
52
|
+
const headerRow = headers
|
|
53
|
+
.map((h, i) => ` ${chalk.bold(h.padEnd(widths[i]))} `)
|
|
54
|
+
.join("│");
|
|
55
|
+
const dataRows = rows.map((r) => r.map((c, i) => ` ${String(c).padEnd(widths[i])} `).join("│"));
|
|
56
|
+
console.log(` ${chalk.dim("┌" + widths.map((w) => "─".repeat(w + 2)).join("┬") + "┐")}`);
|
|
57
|
+
console.log(` ${chalk.dim("│")}${headerRow}${chalk.dim("│")}`);
|
|
58
|
+
console.log(` ${chalk.dim("├" + sep + "┤")}`);
|
|
59
|
+
for (const row of dataRows) {
|
|
60
|
+
console.log(` ${chalk.dim("│")}${row}${chalk.dim("│")}`);
|
|
61
|
+
}
|
|
62
|
+
console.log(` ${chalk.dim("└" + widths.map((w) => "─".repeat(w + 2)).join("┴") + "┘")}`);
|
|
63
|
+
}
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// ERROR PARSING
|
|
66
|
+
// =============================================================================
|
|
67
|
+
/** Known Anchor program errors from the IDL */
|
|
68
|
+
const PROGRAM_ERRORS = {
|
|
69
|
+
6000: "Unauthorized — only the protocol authority can do this",
|
|
70
|
+
6001: "Task is not active",
|
|
71
|
+
6002: "Task has reached its maximum claims",
|
|
72
|
+
6003: "Math overflow",
|
|
73
|
+
6004: "Agent tier is too low for this task",
|
|
74
|
+
6005: "Complete the prerequisite task before submitting this one",
|
|
75
|
+
6006: "Invalid prerequisite account",
|
|
76
|
+
6007: "Task cannot require itself as a prerequisite",
|
|
77
|
+
6008: "Invalid invite code",
|
|
78
|
+
6009: "Invite is inactive",
|
|
79
|
+
6010: "Self-referral is not allowed",
|
|
80
|
+
};
|
|
81
|
+
/** System-level Solana errors */
|
|
82
|
+
const SYSTEM_ERRORS = {
|
|
83
|
+
"already in use": "Account already exists — you may already be registered",
|
|
84
|
+
"insufficient funds": "Not enough SOL in your wallet to pay for the transaction",
|
|
85
|
+
AccountNotFound: "Account not found on-chain",
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Parse a raw Anchor/Solana error into a human-readable message.
|
|
89
|
+
*/
|
|
90
|
+
export function parseError(err) {
|
|
91
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
92
|
+
const maybeLogs = err?.logs || err?.transactionLogs;
|
|
93
|
+
if (Array.isArray(maybeLogs) && maybeLogs.length > 0) {
|
|
94
|
+
const tail = maybeLogs.slice(-3).join(" | ");
|
|
95
|
+
return `${msg} Logs: ${tail}`;
|
|
96
|
+
}
|
|
97
|
+
// Check program errors (custom error code)
|
|
98
|
+
const codeMatch = msg.match(/custom program error: 0x([0-9a-fA-F]+)/);
|
|
99
|
+
if (codeMatch) {
|
|
100
|
+
const code = parseInt(codeMatch[1], 16);
|
|
101
|
+
if (code === 0) {
|
|
102
|
+
// System program error 0x0 = account already in use
|
|
103
|
+
if (msg.includes("already in use")) {
|
|
104
|
+
return "Agent already registered. Run: pc status";
|
|
105
|
+
}
|
|
106
|
+
return "Account already exists on-chain";
|
|
107
|
+
}
|
|
108
|
+
const known = PROGRAM_ERRORS[code];
|
|
109
|
+
if (known)
|
|
110
|
+
return known;
|
|
111
|
+
return `Program error 0x${codeMatch[1]} (${code})`;
|
|
112
|
+
}
|
|
113
|
+
// Check named Anchor errors
|
|
114
|
+
const anchorMatch = msg.match(/Error Code: (\w+)\. .* Error Message: (.+)\./);
|
|
115
|
+
if (anchorMatch) {
|
|
116
|
+
return anchorMatch[2];
|
|
117
|
+
}
|
|
118
|
+
// Check system-level patterns
|
|
119
|
+
for (const [pattern, human] of Object.entries(SYSTEM_ERRORS)) {
|
|
120
|
+
if (msg.toLowerCase().includes(pattern.toLowerCase())) {
|
|
121
|
+
return human;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Fallback: return first meaningful line
|
|
125
|
+
const first = msg.split("\n")[0];
|
|
126
|
+
return first.length > 120 ? first.slice(0, 117) + "..." : first;
|
|
127
|
+
}
|