@silkysquad/silk 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,41 @@
1
+ import { Keypair, Transaction } from '@solana/web3.js';
2
+ import bs58 from 'bs58';
3
+ import { loadConfig, getWallet, getApiUrl, getClaimUrl } from '../config.js';
4
+ import { createHttpClient } from '../client.js';
5
+ import { outputSuccess } from '../output.js';
6
+ import { validatePay } from '../validate.js';
7
+ import { resolveRecipient } from '../contacts.js';
8
+
9
+ export async function pay(recipient: string, amount: string, opts: { memo?: string; wallet?: string }) {
10
+ recipient = resolveRecipient(recipient);
11
+ const config = loadConfig();
12
+ const wallet = getWallet(config, opts.wallet);
13
+ const client = createHttpClient({ baseUrl: getApiUrl(config) });
14
+
15
+ const amountNum = await validatePay(client, recipient, amount, wallet.address);
16
+
17
+ // 1. Build unsigned transaction
18
+ const buildRes = await client.post('/api/tx/create-transfer', {
19
+ sender: wallet.address,
20
+ recipient,
21
+ amount: amountNum,
22
+ token: 'usdc',
23
+ memo: opts.memo || '',
24
+ });
25
+
26
+ const { transaction: txBase64, transferPda } = buildRes.data.data;
27
+
28
+ // 2. Sign the transaction
29
+ const tx = Transaction.from(Buffer.from(txBase64, 'base64'));
30
+ const keypair = Keypair.fromSecretKey(bs58.decode(wallet.privateKey));
31
+ tx.sign(keypair);
32
+
33
+ // 3. Submit signed transaction
34
+ const submitRes = await client.post('/api/tx/submit', {
35
+ signedTx: tx.serialize().toString('base64'),
36
+ });
37
+
38
+ const { txid } = submitRes.data.data;
39
+ const claimUrl = getClaimUrl(config, transferPda);
40
+ outputSuccess({ action: 'pay', transferPda, txid, amount: amountNum, recipient, claimUrl });
41
+ }
@@ -0,0 +1,29 @@
1
+ import { loadConfig, getWallet, getApiUrl } from '../config.js';
2
+ import { createHttpClient } from '../client.js';
3
+ import { SdkError } from '../errors.js';
4
+ import { outputSuccess } from '../output.js';
5
+
6
+ export async function paymentsList(opts: { wallet?: string }) {
7
+ const config = loadConfig();
8
+ const wallet = getWallet(config, opts.wallet);
9
+ const client = createHttpClient({ baseUrl: getApiUrl(config) });
10
+
11
+ const res = await client.get(`/api/transfers`, { params: { wallet: wallet.address } });
12
+ const transfers = res.data.data.transfers;
13
+
14
+ outputSuccess({ transfers });
15
+ }
16
+
17
+ export async function paymentsGet(transferPda: string) {
18
+ const config = loadConfig();
19
+ const client = createHttpClient({ baseUrl: getApiUrl(config) });
20
+
21
+ const res = await client.get(`/api/transfers/${transferPda}`);
22
+ const transfer = res.data.data.transfer;
23
+
24
+ if (!transfer) {
25
+ throw new SdkError('TRANSFER_NOT_FOUND', `Transfer not found: ${transferPda}`);
26
+ }
27
+
28
+ outputSuccess({ transfer });
29
+ }
@@ -0,0 +1,67 @@
1
+ import { Keypair } from '@solana/web3.js';
2
+ import bs58 from 'bs58';
3
+ import { loadConfig, saveConfig, getWallet, getApiUrl } from '../config.js';
4
+ import { createHttpClient } from '../client.js';
5
+ import { SdkError } from '../errors.js';
6
+ import { outputSuccess } from '../output.js';
7
+
8
+ export async function walletCreate(label: string) {
9
+ const config = loadConfig();
10
+
11
+ if (config.wallets.find((w) => w.label === label)) {
12
+ throw new SdkError('WALLET_EXISTS', `Wallet "${label}" already exists.`);
13
+ }
14
+
15
+ const keypair = Keypair.generate();
16
+ const address = keypair.publicKey.toBase58();
17
+ const privateKey = bs58.encode(keypair.secretKey);
18
+
19
+ config.wallets.push({ label, address, privateKey });
20
+ if (config.wallets.length === 1) {
21
+ config.defaultWallet = label;
22
+ }
23
+
24
+ saveConfig(config);
25
+ outputSuccess({ action: 'wallet_created', label, address });
26
+ }
27
+
28
+ export async function walletList() {
29
+ const config = loadConfig();
30
+ const wallets = config.wallets.map((w) => ({
31
+ label: w.label,
32
+ address: w.address,
33
+ default: w.label === config.defaultWallet,
34
+ }));
35
+ outputSuccess({ wallets });
36
+ }
37
+
38
+ export async function walletFund(opts: { sol?: boolean; usdc?: boolean; wallet?: string }) {
39
+ const config = loadConfig();
40
+ const wallet = getWallet(config, opts.wallet);
41
+ const client = createHttpClient({ baseUrl: getApiUrl(config) });
42
+
43
+ const doSol = opts.sol || (!opts.sol && !opts.usdc);
44
+ const doUsdc = opts.usdc || (!opts.sol && !opts.usdc);
45
+
46
+ // Determine the token parameter for a single API call
47
+ let token: string;
48
+ if (doSol && doUsdc) {
49
+ token = 'both';
50
+ } else if (doSol) {
51
+ token = 'sol';
52
+ } else {
53
+ token = 'usdc';
54
+ }
55
+
56
+ const funded: Record<string, unknown> = {};
57
+
58
+ try {
59
+ const res = await client.post('/api/tx/faucet', { wallet: wallet.address, token });
60
+ if (res.data.data.sol) funded.sol = res.data.data.sol;
61
+ if (res.data.data.usdc) funded.usdc = res.data.data.usdc;
62
+ } catch (e: any) {
63
+ funded.error = { code: e.code || 'FAUCET_FAILED', message: e.message };
64
+ }
65
+
66
+ outputSuccess({ action: 'wallet_funded', wallet: wallet.label, address: wallet.address, funded });
67
+ }
package/src/config.ts ADDED
@@ -0,0 +1,101 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { SdkError } from './errors.js';
6
+
7
+ export const CONFIG_DIR = path.join(os.homedir(), '.config', 'silk');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+
10
+ export interface WalletEntry {
11
+ label: string;
12
+ address: string;
13
+ privateKey: string;
14
+ }
15
+
16
+ export interface AccountInfo {
17
+ pda: string;
18
+ owner: string;
19
+ mint: string;
20
+ mintDecimals: number;
21
+ operatorIndex: number;
22
+ perTxLimit: number;
23
+ syncedAt: string;
24
+ }
25
+
26
+ export type SolanaCluster = 'mainnet-beta' | 'devnet';
27
+
28
+ export interface SilkConfig {
29
+ wallets: WalletEntry[];
30
+ defaultWallet: string;
31
+ preferences: Record<string, unknown>;
32
+ apiUrl?: string;
33
+ cluster?: SolanaCluster;
34
+ account?: AccountInfo;
35
+ agentId?: string;
36
+ }
37
+
38
+ function defaultConfig(): SilkConfig {
39
+ return { wallets: [], defaultWallet: 'main', preferences: {}, cluster: 'mainnet-beta' };
40
+ }
41
+
42
+ export function loadConfig(): SilkConfig {
43
+ try {
44
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
45
+ return JSON.parse(raw) as SilkConfig;
46
+ } catch {
47
+ return defaultConfig();
48
+ }
49
+ }
50
+
51
+ export function saveConfig(config: SilkConfig): void {
52
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
53
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
54
+ }
55
+
56
+ export function getWallet(config: SilkConfig, label?: string): WalletEntry {
57
+ const target = label || config.defaultWallet;
58
+ const wallet = config.wallets.find((w) => w.label === target);
59
+ if (!wallet) {
60
+ throw new SdkError('WALLET_NOT_FOUND', `Wallet "${target}" not found. Run: silk wallet create`);
61
+ }
62
+ return wallet;
63
+ }
64
+
65
+ const CLUSTER_API_URLS: Record<SolanaCluster, string> = {
66
+ 'mainnet-beta': 'https://api.silkyway.ai',
67
+ 'devnet': 'https://devnet-api.silkyway.ai',
68
+ };
69
+
70
+ export function getCluster(config: SilkConfig): SolanaCluster {
71
+ return config.cluster || 'mainnet-beta';
72
+ }
73
+
74
+ export function getApiUrl(config: SilkConfig): string {
75
+ return config.apiUrl || process.env.SILK_API_URL || CLUSTER_API_URLS[getCluster(config)];
76
+ }
77
+
78
+ export function ensureAgentId(config: SilkConfig): { agentId: string; created: boolean } {
79
+ if (config.agentId) {
80
+ return { agentId: config.agentId, created: false };
81
+ }
82
+ const agentId = randomUUID();
83
+ config.agentId = agentId;
84
+ return { agentId, created: true };
85
+ }
86
+
87
+ const APP_BASE_URL = 'https://app.silkyway.so';
88
+
89
+ export function getClaimUrl(config: SilkConfig, transferPda: string): string {
90
+ const base = `${APP_BASE_URL}/transfers/${transferPda}`;
91
+ const cluster = getCluster(config);
92
+ return cluster === 'devnet' ? `${base}?cluster=devnet` : base;
93
+ }
94
+
95
+ export function getAgentId(config: SilkConfig): string {
96
+ const result = ensureAgentId(config);
97
+ if (result.created) {
98
+ saveConfig(config);
99
+ }
100
+ return result.agentId;
101
+ }
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PublicKey } from '@solana/web3.js';
4
+ import { CONFIG_DIR } from './config.js';
5
+ import { SdkError } from './errors.js';
6
+
7
+ const CONTACTS_FILE = path.join(CONFIG_DIR, 'contacts.json');
8
+
9
+ export interface Contact {
10
+ name: string;
11
+ address: string;
12
+ }
13
+
14
+ export interface ContactsStore {
15
+ contacts: Contact[];
16
+ }
17
+
18
+ export function loadContacts(): ContactsStore {
19
+ try {
20
+ const raw = fs.readFileSync(CONTACTS_FILE, 'utf-8');
21
+ return JSON.parse(raw) as ContactsStore;
22
+ } catch {
23
+ return { contacts: [] };
24
+ }
25
+ }
26
+
27
+ export function saveContacts(store: ContactsStore): void {
28
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
29
+ fs.writeFileSync(CONTACTS_FILE, JSON.stringify(store, null, 2), 'utf-8');
30
+ }
31
+
32
+ function isValidSolanaAddress(address: string): boolean {
33
+ try {
34
+ new PublicKey(address);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ export function addContact(name: string, address: string): void {
42
+ const normalized = name.toLowerCase();
43
+
44
+ if (isValidSolanaAddress(normalized)) {
45
+ throw new SdkError('INVALID_CONTACT_NAME', 'Contact name cannot be a valid Solana address');
46
+ }
47
+
48
+ if (!isValidSolanaAddress(address)) {
49
+ throw new SdkError('INVALID_ADDRESS', `"${address}" is not a valid Solana address`);
50
+ }
51
+
52
+ const store = loadContacts();
53
+ const existing = store.contacts.find((c) => c.name === normalized);
54
+ if (existing) {
55
+ throw new SdkError('CONTACT_EXISTS', `Contact "${normalized}" already exists (${existing.address})`);
56
+ }
57
+
58
+ store.contacts.push({ name: normalized, address });
59
+ saveContacts(store);
60
+ }
61
+
62
+ export function removeContact(name: string): void {
63
+ const normalized = name.toLowerCase();
64
+ const store = loadContacts();
65
+ const index = store.contacts.findIndex((c) => c.name === normalized);
66
+ if (index === -1) {
67
+ throw new SdkError('CONTACT_NOT_FOUND', `Contact "${normalized}" not found`);
68
+ }
69
+ store.contacts.splice(index, 1);
70
+ saveContacts(store);
71
+ }
72
+
73
+ export function getContact(name: string): Contact | null {
74
+ const normalized = name.toLowerCase();
75
+ const store = loadContacts();
76
+ return store.contacts.find((c) => c.name === normalized) || null;
77
+ }
78
+
79
+ export function listContacts(): Contact[] {
80
+ return loadContacts().contacts;
81
+ }
82
+
83
+ export function resolveRecipient(recipient: string): string {
84
+ const contact = getContact(recipient);
85
+ return contact ? contact.address : recipient;
86
+ }
87
+
88
+ export function initContacts(): boolean {
89
+ if (fs.existsSync(CONTACTS_FILE)) {
90
+ return false;
91
+ }
92
+ saveContacts({ contacts: [] });
93
+ return true;
94
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,95 @@
1
+ export class SdkError extends Error {
2
+ constructor(
3
+ public readonly code: string,
4
+ message: string,
5
+ ) {
6
+ super(message);
7
+ this.name = 'SdkError';
8
+ }
9
+ }
10
+
11
+ export const ANCHOR_ERROR_MAP: Record<number, { code: string; message: string }> = {
12
+ 6000: { code: 'ANCHOR_MATH_OVERFLOW', message: 'Mathematical overflow occurred' },
13
+ 6001: { code: 'ANCHOR_TRANSFER_NOT_ACTIVE', message: 'Transfer is not in active status' },
14
+ 6002: { code: 'ANCHOR_CANNOT_CLAIM', message: 'Claim deadline has passed' },
15
+ 6003: { code: 'ANCHOR_CONDITIONS_NOT_MET', message: 'Release conditions not met' },
16
+ 6004: { code: 'ANCHOR_INVALID_CONDITION', message: 'Invalid condition parameters' },
17
+ 6005: { code: 'ANCHOR_INSUFFICIENT_FUNDS', message: 'Insufficient funds in vault' },
18
+ 6006: { code: 'ANCHOR_POOL_PAUSED', message: 'Pool is paused' },
19
+ 6007: { code: 'ANCHOR_UNAUTHORIZED', message: 'Unauthorized action' },
20
+ 6008: { code: 'ANCHOR_INVALID_TIME_WINDOW', message: 'Invalid time window' },
21
+ 6009: { code: 'ANCHOR_DEPOSIT_TOO_SMALL', message: 'Deposit amount too small' },
22
+ 6010: { code: 'ANCHOR_INVALID_FEE_CONFIG', message: 'Invalid fee configuration' },
23
+ 6011: { code: 'ANCHOR_INVALID_TRANSFER_FEE', message: 'Invalid transfer fee' },
24
+ 6012: { code: 'ANCHOR_TRANSFER_ALREADY_CLAIMED', message: 'Transfer already claimed' },
25
+ 6013: { code: 'ANCHOR_TRANSFER_ALREADY_CANCELLED', message: 'Transfer already cancelled' },
26
+ 6014: { code: 'ANCHOR_TRANSFER_ALREADY_REJECTED', message: 'Transfer already rejected' },
27
+ 6015: { code: 'ANCHOR_TRANSFER_EXPIRED', message: 'Transfer is expired' },
28
+ 6016: { code: 'ANCHOR_ONLY_SENDER_CAN_CANCEL', message: 'Only sender can cancel transfer' },
29
+ 6017: { code: 'ANCHOR_ONLY_RECIPIENT_CAN_CLAIM', message: 'Only recipient can claim transfer' },
30
+ 6018: { code: 'ANCHOR_ONLY_OPERATOR_CAN_REJECT', message: 'Only operator can reject transfer' },
31
+ 6019: { code: 'ANCHOR_INVALID_MEMO_LENGTH', message: 'Invalid memo length' },
32
+ 6020: { code: 'ANCHOR_CLAIM_DEADLINE_NOT_PASSED', message: 'Claim deadline has not passed' },
33
+ 6021: { code: 'ANCHOR_CALCULATION_ERROR', message: 'Calculation error' },
34
+ 6022: { code: 'ANCHOR_MISSING_ACCOUNT', message: 'Missing required account' },
35
+ 6023: { code: 'ANCHOR_INVALID_MINT', message: 'Invalid mint' },
36
+ 6024: { code: 'ANCHOR_STALE_POOL_VALUE', message: 'Pool value is stale and must be updated' },
37
+ 6025: { code: 'ANCHOR_INVALID_OPERATION', message: 'Invalid operation for this pool type' },
38
+ 6026: { code: 'ANCHOR_OUTSTANDING_TRANSFERS', message: 'Cannot reset pool with outstanding transfers' },
39
+ 6027: { code: 'ANCHOR_INVALID_TRANSFER', message: 'Invalid transfer' },
40
+ 6028: { code: 'ANCHOR_TRANSFER_ALREADY_DECLINED', message: 'Transfer already declined' },
41
+ 6029: { code: 'ANCHOR_ONLY_RECIPIENT_CAN_DECLINE', message: 'Only recipient can decline transfer' },
42
+ };
43
+
44
+ export const SILKYSIG_ERROR_MAP: Record<number, { code: string; message: string }> = {
45
+ 6000: { code: 'POLICY_UNAUTHORIZED', message: 'Unauthorized: signer is not owner or operator' },
46
+ 6001: { code: 'POLICY_EXCEEDS_TX_LIMIT', message: 'Transfer exceeds operator per-transaction limit' },
47
+ 6002: { code: 'ACCOUNT_PAUSED', message: 'Account is paused' },
48
+ 6003: { code: 'MAX_OPERATORS', message: 'Maximum operators reached' },
49
+ 6004: { code: 'OPERATOR_NOT_FOUND', message: 'Operator not found' },
50
+ 6005: { code: 'OPERATOR_EXISTS', message: 'Operator slot already occupied' },
51
+ 6006: { code: 'MATH_OVERFLOW', message: 'Mathematical overflow' },
52
+ 6007: { code: 'AMOUNT_MUST_BE_POSITIVE', message: 'Amount must be positive' },
53
+ 6008: { code: 'DRIFT_USER_ALREADY_INITIALIZED', message: 'Drift user already initialized' },
54
+ 6009: { code: 'DRIFT_DEPOSIT_FAILED', message: 'Drift deposit failed' },
55
+ 6010: { code: 'DRIFT_WITHDRAW_FAILED', message: 'Drift withdrawal failed' },
56
+ 6011: { code: 'INVALID_DRIFT_USER', message: 'Invalid Drift user account' },
57
+ 6012: { code: 'MISSING_DRIFT_ACCOUNTS', message: 'Missing required Drift accounts' },
58
+ 6013: { code: 'INVALID_DRIFT_PROGRAM', message: 'Invalid Drift program ID' },
59
+ 6014: { code: 'DRIFT_DELETE_USER_FAILED', message: 'Failed to delete Drift user' },
60
+ };
61
+
62
+ export function toSilkysigError(err: unknown): SdkError {
63
+ if (err instanceof SdkError) return err;
64
+
65
+ const message = err instanceof Error ? err.message : String(err);
66
+
67
+ const hexMatch = message.match(/0x([0-9a-fA-F]+)/);
68
+ if (hexMatch) {
69
+ const errorCode = parseInt(hexMatch[1], 16);
70
+ const mapped = SILKYSIG_ERROR_MAP[errorCode];
71
+ if (mapped) {
72
+ return new SdkError(mapped.code, mapped.message);
73
+ }
74
+ }
75
+
76
+ return new SdkError('UNKNOWN_ERROR', message);
77
+ }
78
+
79
+ export function toSdkError(err: unknown): SdkError {
80
+ if (err instanceof SdkError) return err;
81
+
82
+ const message = err instanceof Error ? err.message : String(err);
83
+
84
+ // Parse Anchor hex error codes from simulation messages (e.g. "0x1770")
85
+ const hexMatch = message.match(/0x([0-9a-fA-F]+)/);
86
+ if (hexMatch) {
87
+ const errorCode = parseInt(hexMatch[1], 16);
88
+ const anchor = ANCHOR_ERROR_MAP[errorCode];
89
+ if (anchor) {
90
+ return new SdkError(anchor.code, anchor.message);
91
+ }
92
+ }
93
+
94
+ return new SdkError('UNKNOWN_ERROR', message);
95
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { loadConfig, saveConfig, getWallet, getApiUrl, CONFIG_DIR } from './config.js';
2
+ export type { SilkConfig, WalletEntry } from './config.js';
3
+ export { loadContacts, saveContacts, addContact, removeContact, getContact, listContacts, resolveRecipient, initContacts } from './contacts.js';
4
+ export type { Contact, ContactsStore } from './contacts.js';
5
+ export { createHttpClient } from './client.js';
6
+ export type { ClientConfig } from './client.js';
7
+ export { getTransfer } from './transfers.js';
8
+ export type { TransferInfo, TokenInfo, PoolInfo } from './transfers.js';
9
+ export { SdkError, ANCHOR_ERROR_MAP, toSdkError } from './errors.js';
10
+ export { outputSuccess, outputError, wrapCommand } from './output.js';
11
+ export { validateAddress, validateAmount, fetchTransfer, validateClaim, validateCancel, validatePay } from './validate.js';
package/src/output.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { SdkError, toSdkError } from './errors.js';
2
+
3
+ function isHuman(): boolean {
4
+ return process.argv.includes('--human');
5
+ }
6
+
7
+ export function outputSuccess(data: Record<string, unknown>): void {
8
+ if (isHuman()) {
9
+ for (const [key, value] of Object.entries(data)) {
10
+ if (typeof value === 'object' && value !== null) {
11
+ console.log(`${key}:`);
12
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
13
+ console.log(` ${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`);
14
+ }
15
+ } else {
16
+ console.log(`${key}: ${value}`);
17
+ }
18
+ }
19
+ return;
20
+ }
21
+ console.log(JSON.stringify({ ok: true, data }));
22
+ }
23
+
24
+ export function outputError(err: SdkError): never {
25
+ if (isHuman()) {
26
+ console.log(`Error [${err.code}]: ${err.message}`);
27
+ } else {
28
+ console.log(JSON.stringify({ ok: false, error: err.code, message: err.message }));
29
+ }
30
+ process.exit(1);
31
+ }
32
+
33
+ export function wrapCommand<T extends (...args: any[]) => Promise<void>>(fn: T): T {
34
+ const wrapped = async (...args: any[]) => {
35
+ try {
36
+ await fn(...args);
37
+ } catch (err) {
38
+ outputError(toSdkError(err));
39
+ }
40
+ };
41
+ return wrapped as unknown as T;
42
+ }
@@ -0,0 +1,49 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { createHttpClient, ClientConfig } from './client.js';
3
+ import { SdkError } from './errors.js';
4
+
5
+ export interface TokenInfo {
6
+ mint: string;
7
+ name: string;
8
+ symbol: string;
9
+ decimals: number;
10
+ }
11
+
12
+ export interface PoolInfo {
13
+ poolPda: string;
14
+ poolId: string;
15
+ operatorKey: string;
16
+ feeBps: number;
17
+ }
18
+
19
+ export interface TransferInfo {
20
+ transferPda: string;
21
+ sender: string;
22
+ recipient: string;
23
+ amount: string;
24
+ amountRaw: string;
25
+ status: string;
26
+ memo?: string;
27
+ token: TokenInfo;
28
+ pool: PoolInfo;
29
+ createTxid: string;
30
+ claimTxid?: string;
31
+ cancelTxid?: string;
32
+ claimableAfter?: string;
33
+ claimableUntil?: string;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ }
37
+
38
+ export async function getTransfer(
39
+ transferPda: string,
40
+ config?: ClientConfig,
41
+ ): Promise<TransferInfo> {
42
+ const client = createHttpClient(config);
43
+ const res = await client.get(`/api/transfers/${transferPda}`);
44
+ const transfer = res.data.data.transfer;
45
+ if (!transfer) {
46
+ throw new SdkError('TRANSFER_NOT_FOUND', `Transfer not found: ${transferPda}`);
47
+ }
48
+ return transfer as TransferInfo;
49
+ }
@@ -0,0 +1,80 @@
1
+ import { PublicKey } from '@solana/web3.js';
2
+ import { AxiosInstance } from 'axios';
3
+ import { SdkError } from './errors.js';
4
+ import { TransferInfo } from './transfers.js';
5
+
6
+ export function validateAddress(address: string, field: string): void {
7
+ try {
8
+ new PublicKey(address);
9
+ } catch {
10
+ throw new SdkError('INVALID_ADDRESS', `Invalid ${field} address: ${address}`);
11
+ }
12
+ }
13
+
14
+ export function validateAmount(amount: string): number {
15
+ const num = parseFloat(amount);
16
+ if (isNaN(num) || num <= 0) {
17
+ throw new SdkError('INVALID_AMOUNT', `Amount must be a positive number, got: ${amount}`);
18
+ }
19
+ return num;
20
+ }
21
+
22
+ export async function fetchTransfer(client: AxiosInstance, transferPda: string): Promise<TransferInfo> {
23
+ const res = await client.get(`/api/transfers/${transferPda}`);
24
+ const transfer = res.data?.data?.transfer;
25
+ if (!transfer) {
26
+ throw new SdkError('TRANSFER_NOT_FOUND', `Transfer not found: ${transferPda}`);
27
+ }
28
+ return transfer as TransferInfo;
29
+ }
30
+
31
+ export async function validateClaim(client: AxiosInstance, transferPda: string, claimerAddress: string): Promise<void> {
32
+ const transfer = await fetchTransfer(client, transferPda);
33
+
34
+ if (transfer.status !== 'ACTIVE') {
35
+ throw new SdkError('TRANSFER_NOT_ACTIVE', `Transfer is ${transfer.status}, not ACTIVE`);
36
+ }
37
+
38
+ if (transfer.recipient !== claimerAddress) {
39
+ throw new SdkError('NOT_RECIPIENT', `Wallet ${claimerAddress} is not the recipient. Recipient is ${transfer.recipient}`);
40
+ }
41
+
42
+ const now = Date.now();
43
+ if (transfer.claimableAfter && now < new Date(transfer.claimableAfter).getTime()) {
44
+ throw new SdkError('CLAIM_WINDOW_NOT_OPEN', `Claim window opens at ${transfer.claimableAfter}`);
45
+ }
46
+ if (transfer.claimableUntil && now > new Date(transfer.claimableUntil).getTime()) {
47
+ throw new SdkError('CLAIM_WINDOW_CLOSED', `Claim window closed at ${transfer.claimableUntil}`);
48
+ }
49
+ }
50
+
51
+ export async function validateCancel(client: AxiosInstance, transferPda: string, cancellerAddress: string): Promise<void> {
52
+ const transfer = await fetchTransfer(client, transferPda);
53
+
54
+ if (transfer.status !== 'ACTIVE') {
55
+ throw new SdkError('TRANSFER_NOT_ACTIVE', `Transfer is ${transfer.status}, not ACTIVE`);
56
+ }
57
+
58
+ if (transfer.sender !== cancellerAddress) {
59
+ throw new SdkError('NOT_SENDER', `Wallet ${cancellerAddress} is not the sender. Sender is ${transfer.sender}`);
60
+ }
61
+ }
62
+
63
+ export async function validatePay(client: AxiosInstance, recipient: string, amount: string, senderAddress: string): Promise<number> {
64
+ validateAddress(recipient, 'recipient');
65
+ const amountNum = validateAmount(amount);
66
+
67
+ try {
68
+ const res = await client.get(`/api/wallet/${senderAddress}/balance`);
69
+ const tokens = res.data?.data?.tokens || [];
70
+ const usdc = tokens.find((t: any) => t.symbol === 'USDC');
71
+ if (usdc && parseFloat(usdc.balance) < amountNum) {
72
+ throw new SdkError('INSUFFICIENT_BALANCE', `Insufficient USDC balance: ${usdc.balance} < ${amountNum}`);
73
+ }
74
+ } catch (err) {
75
+ if (err instanceof SdkError) throw err;
76
+ // Balance check is best-effort; don't fail if the API is unreachable
77
+ }
78
+
79
+ return amountNum;
80
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "declaration": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }