@ottocode/sdk 0.1.173
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/README.md +338 -0
- package/package.json +128 -0
- package/src/agent/types.ts +19 -0
- package/src/auth/src/copilot-oauth.ts +190 -0
- package/src/auth/src/index.ts +100 -0
- package/src/auth/src/oauth.ts +234 -0
- package/src/auth/src/openai-oauth.ts +394 -0
- package/src/auth/src/wallet.ts +51 -0
- package/src/browser.ts +32 -0
- package/src/config/src/index.ts +110 -0
- package/src/config/src/manager.ts +181 -0
- package/src/config/src/paths.ts +98 -0
- package/src/core/src/errors.ts +102 -0
- package/src/core/src/index.ts +108 -0
- package/src/core/src/providers/resolver.ts +244 -0
- package/src/core/src/streaming/artifacts.ts +41 -0
- package/src/core/src/terminals/bun-pty.ts +13 -0
- package/src/core/src/terminals/circular-buffer.ts +30 -0
- package/src/core/src/terminals/ensure-bun-pty.ts +70 -0
- package/src/core/src/terminals/index.ts +8 -0
- package/src/core/src/terminals/manager.ts +158 -0
- package/src/core/src/terminals/rust-libs.ts +30 -0
- package/src/core/src/terminals/terminal.ts +132 -0
- package/src/core/src/tools/bin-manager.ts +250 -0
- package/src/core/src/tools/builtin/bash.ts +155 -0
- package/src/core/src/tools/builtin/bash.txt +7 -0
- package/src/core/src/tools/builtin/file-cache.ts +39 -0
- package/src/core/src/tools/builtin/finish.ts +12 -0
- package/src/core/src/tools/builtin/finish.txt +10 -0
- package/src/core/src/tools/builtin/fs/cd.ts +19 -0
- package/src/core/src/tools/builtin/fs/cd.txt +5 -0
- package/src/core/src/tools/builtin/fs/index.ts +20 -0
- package/src/core/src/tools/builtin/fs/ls.ts +72 -0
- package/src/core/src/tools/builtin/fs/ls.txt +8 -0
- package/src/core/src/tools/builtin/fs/pwd.ts +17 -0
- package/src/core/src/tools/builtin/fs/pwd.txt +5 -0
- package/src/core/src/tools/builtin/fs/read.ts +119 -0
- package/src/core/src/tools/builtin/fs/read.txt +8 -0
- package/src/core/src/tools/builtin/fs/tree.ts +149 -0
- package/src/core/src/tools/builtin/fs/tree.txt +11 -0
- package/src/core/src/tools/builtin/fs/util.ts +95 -0
- package/src/core/src/tools/builtin/fs/write.ts +106 -0
- package/src/core/src/tools/builtin/fs/write.txt +11 -0
- package/src/core/src/tools/builtin/git.commit.txt +6 -0
- package/src/core/src/tools/builtin/git.diff.txt +5 -0
- package/src/core/src/tools/builtin/git.status.txt +5 -0
- package/src/core/src/tools/builtin/git.ts +151 -0
- package/src/core/src/tools/builtin/glob.ts +128 -0
- package/src/core/src/tools/builtin/glob.txt +10 -0
- package/src/core/src/tools/builtin/grep.ts +136 -0
- package/src/core/src/tools/builtin/grep.txt +9 -0
- package/src/core/src/tools/builtin/ignore.ts +45 -0
- package/src/core/src/tools/builtin/patch/apply.ts +546 -0
- package/src/core/src/tools/builtin/patch/constants.ts +5 -0
- package/src/core/src/tools/builtin/patch/normalize.ts +31 -0
- package/src/core/src/tools/builtin/patch/parse-enveloped.ts +209 -0
- package/src/core/src/tools/builtin/patch/parse-unified.ts +231 -0
- package/src/core/src/tools/builtin/patch/parse.ts +28 -0
- package/src/core/src/tools/builtin/patch/text.ts +23 -0
- package/src/core/src/tools/builtin/patch/types.ts +82 -0
- package/src/core/src/tools/builtin/patch.ts +167 -0
- package/src/core/src/tools/builtin/patch.txt +207 -0
- package/src/core/src/tools/builtin/progress.ts +55 -0
- package/src/core/src/tools/builtin/progress.txt +7 -0
- package/src/core/src/tools/builtin/ripgrep.ts +125 -0
- package/src/core/src/tools/builtin/ripgrep.txt +7 -0
- package/src/core/src/tools/builtin/terminal.ts +300 -0
- package/src/core/src/tools/builtin/terminal.txt +93 -0
- package/src/core/src/tools/builtin/todos.ts +66 -0
- package/src/core/src/tools/builtin/todos.txt +7 -0
- package/src/core/src/tools/builtin/websearch.ts +250 -0
- package/src/core/src/tools/builtin/websearch.txt +12 -0
- package/src/core/src/tools/error.ts +67 -0
- package/src/core/src/tools/loader.ts +421 -0
- package/src/core/src/types/index.ts +11 -0
- package/src/core/src/types/types.ts +4 -0
- package/src/core/src/utils/ansi.ts +27 -0
- package/src/core/src/utils/debug.ts +40 -0
- package/src/core/src/utils/logger.ts +150 -0
- package/src/index.ts +313 -0
- package/src/prompts/src/agents/build.txt +89 -0
- package/src/prompts/src/agents/general.txt +15 -0
- package/src/prompts/src/agents/plan.txt +10 -0
- package/src/prompts/src/agents/research.txt +50 -0
- package/src/prompts/src/base.txt +24 -0
- package/src/prompts/src/debug.ts +104 -0
- package/src/prompts/src/index.ts +1 -0
- package/src/prompts/src/modes/oneshot.txt +9 -0
- package/src/prompts/src/providers/anthropic.txt +247 -0
- package/src/prompts/src/providers/anthropicSpoof.txt +1 -0
- package/src/prompts/src/providers/default.txt +466 -0
- package/src/prompts/src/providers/google.txt +230 -0
- package/src/prompts/src/providers/moonshot.txt +24 -0
- package/src/prompts/src/providers/openai.txt +414 -0
- package/src/prompts/src/providers.ts +143 -0
- package/src/providers/src/anthropic-caching.ts +202 -0
- package/src/providers/src/anthropic-oauth-client.ts +157 -0
- package/src/providers/src/authorization.ts +17 -0
- package/src/providers/src/catalog-manual.ts +135 -0
- package/src/providers/src/catalog-merged.ts +9 -0
- package/src/providers/src/catalog.ts +8329 -0
- package/src/providers/src/copilot-client.ts +39 -0
- package/src/providers/src/env.ts +31 -0
- package/src/providers/src/google-client.ts +16 -0
- package/src/providers/src/index.ts +75 -0
- package/src/providers/src/moonshot-client.ts +25 -0
- package/src/providers/src/oauth-models.ts +39 -0
- package/src/providers/src/openai-oauth-client.ts +108 -0
- package/src/providers/src/opencode-client.ts +64 -0
- package/src/providers/src/openrouter-client.ts +31 -0
- package/src/providers/src/pricing.ts +178 -0
- package/src/providers/src/setu-client.ts +643 -0
- package/src/providers/src/utils.ts +210 -0
- package/src/providers/src/validate.ts +39 -0
- package/src/providers/src/zai-client.ts +47 -0
- package/src/skills/index.ts +34 -0
- package/src/skills/loader.ts +152 -0
- package/src/skills/parser.ts +108 -0
- package/src/skills/tool.ts +87 -0
- package/src/skills/types.ts +41 -0
- package/src/skills/validator.ts +110 -0
- package/src/types/src/auth.ts +33 -0
- package/src/types/src/config.ts +36 -0
- package/src/types/src/index.ts +20 -0
- package/src/types/src/provider.ts +71 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { Keypair } from '@solana/web3.js';
|
|
3
|
+
import bs58 from 'bs58';
|
|
4
|
+
import { createPaymentHeader } from 'x402/client';
|
|
5
|
+
import type { PaymentRequirements } from 'x402/types';
|
|
6
|
+
import { svm } from 'x402/shared';
|
|
7
|
+
import nacl from 'tweetnacl';
|
|
8
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
9
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
10
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
11
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
12
|
+
import { addAnthropicCacheControl } from './anthropic-caching.ts';
|
|
13
|
+
|
|
14
|
+
function simplifyPaymentError(errMsg: string): string {
|
|
15
|
+
const lower = errMsg.toLowerCase();
|
|
16
|
+
if (
|
|
17
|
+
lower.includes('insufficient') ||
|
|
18
|
+
lower.includes('not enough') ||
|
|
19
|
+
lower.includes('balance')
|
|
20
|
+
) {
|
|
21
|
+
return 'Insufficient USDC balance';
|
|
22
|
+
}
|
|
23
|
+
if (lower.includes('simulation') || lower.includes('compute unit')) {
|
|
24
|
+
return 'Transaction simulation failed';
|
|
25
|
+
}
|
|
26
|
+
if (lower.includes('blockhash') || lower.includes('expired')) {
|
|
27
|
+
return 'Transaction expired, please retry';
|
|
28
|
+
}
|
|
29
|
+
if (lower.includes('timeout') || lower.includes('timed out')) {
|
|
30
|
+
return 'Transaction timed out';
|
|
31
|
+
}
|
|
32
|
+
if (lower.includes('rejected') || lower.includes('cancelled')) {
|
|
33
|
+
return 'Transaction rejected';
|
|
34
|
+
}
|
|
35
|
+
if (lower.includes('network') || lower.includes('connection')) {
|
|
36
|
+
return 'Network error';
|
|
37
|
+
}
|
|
38
|
+
const short = errMsg.split('.')[0].slice(0, 80);
|
|
39
|
+
return short.length < errMsg.length ? `${short}...` : errMsg;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_BASE_URL = 'https://api.setu.nitish.sh';
|
|
43
|
+
const DEFAULT_RPC_URL = 'https://api.mainnet-beta.solana.com';
|
|
44
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
45
|
+
const DEFAULT_MAX_PAYMENT_ATTEMPTS = 20;
|
|
46
|
+
|
|
47
|
+
export type SetuPaymentCallbacks = {
|
|
48
|
+
onPaymentRequired?: (amountUsd: number, currentBalance?: number) => void;
|
|
49
|
+
onPaymentSigning?: () => void;
|
|
50
|
+
onPaymentComplete?: (data: {
|
|
51
|
+
amountUsd: number;
|
|
52
|
+
newBalance: number;
|
|
53
|
+
transactionId?: string;
|
|
54
|
+
}) => void;
|
|
55
|
+
onPaymentError?: (error: string) => void;
|
|
56
|
+
onPaymentApproval?: (info: {
|
|
57
|
+
amountUsd: number;
|
|
58
|
+
currentBalance: number;
|
|
59
|
+
}) => Promise<'crypto' | 'fiat' | 'cancel'>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type SetuProviderOptions = {
|
|
63
|
+
baseURL?: string;
|
|
64
|
+
rpcURL?: string;
|
|
65
|
+
network?: string;
|
|
66
|
+
maxRequestAttempts?: number;
|
|
67
|
+
maxPaymentAttempts?: number;
|
|
68
|
+
callbacks?: SetuPaymentCallbacks;
|
|
69
|
+
providerNpm?: string;
|
|
70
|
+
promptCacheKey?: string;
|
|
71
|
+
promptCacheRetention?: 'in_memory' | '24h';
|
|
72
|
+
topupApprovalMode?: 'auto' | 'approval';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type SetuAuth = {
|
|
76
|
+
privateKey: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ExactPaymentRequirement = {
|
|
80
|
+
scheme: 'exact';
|
|
81
|
+
network: string;
|
|
82
|
+
maxAmountRequired: string;
|
|
83
|
+
asset: string;
|
|
84
|
+
payTo: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
resource?: string;
|
|
87
|
+
extra?: Record<string, unknown>;
|
|
88
|
+
maxTimeoutSeconds?: number;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type PaymentPayload = {
|
|
92
|
+
x402Version: 1;
|
|
93
|
+
scheme: 'exact';
|
|
94
|
+
network: string;
|
|
95
|
+
payload: { transaction: string };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
type PaymentResponse = {
|
|
99
|
+
amount_usd?: number | string;
|
|
100
|
+
new_balance?: number | string;
|
|
101
|
+
amount?: number;
|
|
102
|
+
balance?: number;
|
|
103
|
+
transaction?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type PaymentQueueEntry = {
|
|
107
|
+
promise: Promise<void>;
|
|
108
|
+
resolve: () => void;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const paymentQueues = new Map<string, PaymentQueueEntry>();
|
|
112
|
+
const globalPaymentAttempts = new Map<string, number>();
|
|
113
|
+
|
|
114
|
+
async function acquirePaymentLock(walletAddress: string): Promise<() => void> {
|
|
115
|
+
const existing = paymentQueues.get(walletAddress);
|
|
116
|
+
|
|
117
|
+
let resolveFunc: () => void = () => {};
|
|
118
|
+
const newPromise = new Promise<void>((resolve) => {
|
|
119
|
+
resolveFunc = resolve;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const entry: PaymentQueueEntry = {
|
|
123
|
+
promise: newPromise,
|
|
124
|
+
resolve: resolveFunc,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
paymentQueues.set(walletAddress, entry);
|
|
128
|
+
|
|
129
|
+
if (existing) {
|
|
130
|
+
console.log('[Setu] Waiting for pending payment to complete...');
|
|
131
|
+
await existing.promise;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
if (paymentQueues.get(walletAddress) === entry) {
|
|
136
|
+
paymentQueues.delete(walletAddress);
|
|
137
|
+
}
|
|
138
|
+
resolveFunc();
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createSetuFetch(
|
|
143
|
+
auth: SetuAuth,
|
|
144
|
+
options: SetuProviderOptions = {},
|
|
145
|
+
): typeof fetch {
|
|
146
|
+
const privateKeyBytes = bs58.decode(auth.privateKey);
|
|
147
|
+
const keypair = Keypair.fromSecretKey(privateKeyBytes);
|
|
148
|
+
const walletAddress = keypair.publicKey.toBase58();
|
|
149
|
+
const baseURL = trimTrailingSlash(options.baseURL ?? DEFAULT_BASE_URL);
|
|
150
|
+
const rpcURL = options.rpcURL ?? DEFAULT_RPC_URL;
|
|
151
|
+
const maxAttempts = options.maxRequestAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
152
|
+
const maxPaymentAttempts =
|
|
153
|
+
options.maxPaymentAttempts ?? DEFAULT_MAX_PAYMENT_ATTEMPTS;
|
|
154
|
+
const callbacks = options.callbacks ?? {};
|
|
155
|
+
const promptCacheKey = options.promptCacheKey;
|
|
156
|
+
const promptCacheRetention = options.promptCacheRetention;
|
|
157
|
+
const topupApprovalMode = options.topupApprovalMode ?? 'auto';
|
|
158
|
+
|
|
159
|
+
const baseFetch = globalThis.fetch.bind(globalThis);
|
|
160
|
+
|
|
161
|
+
const buildWalletHeaders = () => {
|
|
162
|
+
const nonce = Date.now().toString();
|
|
163
|
+
const signature = signNonce(nonce, privateKeyBytes);
|
|
164
|
+
return {
|
|
165
|
+
'x-wallet-address': walletAddress,
|
|
166
|
+
'x-wallet-nonce': nonce,
|
|
167
|
+
'x-wallet-signature': signature,
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return async (
|
|
172
|
+
input: Parameters<typeof fetch>[0],
|
|
173
|
+
init?: Parameters<typeof fetch>[1],
|
|
174
|
+
) => {
|
|
175
|
+
let attempt = 0;
|
|
176
|
+
|
|
177
|
+
while (attempt < maxAttempts) {
|
|
178
|
+
attempt++;
|
|
179
|
+
let body = init?.body;
|
|
180
|
+
if (body && typeof body === 'string') {
|
|
181
|
+
try {
|
|
182
|
+
const parsed = JSON.parse(body);
|
|
183
|
+
|
|
184
|
+
if (promptCacheKey) parsed.prompt_cache_key = promptCacheKey;
|
|
185
|
+
if (promptCacheRetention)
|
|
186
|
+
parsed.prompt_cache_retention = promptCacheRetention;
|
|
187
|
+
|
|
188
|
+
addAnthropicCacheControl(parsed);
|
|
189
|
+
body = JSON.stringify(parsed);
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
const headers = new Headers(init?.headers);
|
|
193
|
+
const walletHeaders = buildWalletHeaders();
|
|
194
|
+
headers.set('x-wallet-address', walletHeaders['x-wallet-address']);
|
|
195
|
+
headers.set('x-wallet-nonce', walletHeaders['x-wallet-nonce']);
|
|
196
|
+
headers.set('x-wallet-signature', walletHeaders['x-wallet-signature']);
|
|
197
|
+
const response = await baseFetch(input, { ...init, body, headers });
|
|
198
|
+
|
|
199
|
+
if (response.status !== 402) {
|
|
200
|
+
return response;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const payload = await response.json().catch(() => ({}));
|
|
204
|
+
const requirement = pickPaymentRequirement(payload);
|
|
205
|
+
if (!requirement) {
|
|
206
|
+
callbacks.onPaymentError?.('Unsupported payment requirement');
|
|
207
|
+
throw new Error('Setu: unsupported payment requirement');
|
|
208
|
+
}
|
|
209
|
+
if (attempt >= maxAttempts) {
|
|
210
|
+
callbacks.onPaymentError?.('Payment failed after multiple attempts');
|
|
211
|
+
throw new Error('Setu: payment failed after multiple attempts');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const currentAttempts = globalPaymentAttempts.get(walletAddress) ?? 0;
|
|
215
|
+
const remainingPayments = maxPaymentAttempts - currentAttempts;
|
|
216
|
+
if (remainingPayments <= 0) {
|
|
217
|
+
callbacks.onPaymentError?.('Maximum payment attempts exceeded');
|
|
218
|
+
throw new Error('Setu: payment failed after maximum payment attempts.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const releaseLock = await acquirePaymentLock(walletAddress);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const amountUsd =
|
|
225
|
+
parseInt(requirement.maxAmountRequired, 10) / 1_000_000;
|
|
226
|
+
|
|
227
|
+
if (topupApprovalMode === 'approval' && callbacks.onPaymentApproval) {
|
|
228
|
+
const approval = await callbacks.onPaymentApproval({
|
|
229
|
+
amountUsd,
|
|
230
|
+
currentBalance: 0,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (approval === 'cancel') {
|
|
234
|
+
callbacks.onPaymentError?.('Payment cancelled by user');
|
|
235
|
+
throw new Error('Setu: payment cancelled by user');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (approval === 'fiat') {
|
|
239
|
+
const err = new Error('Setu: fiat payment selected');
|
|
240
|
+
(err as Error & { code: string }).code = 'SETU_FIAT_SELECTED';
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
callbacks.onPaymentRequired?.(amountUsd, 0);
|
|
246
|
+
|
|
247
|
+
const outcome = await handlePayment({
|
|
248
|
+
requirement,
|
|
249
|
+
keypair,
|
|
250
|
+
rpcURL,
|
|
251
|
+
baseURL,
|
|
252
|
+
baseFetch,
|
|
253
|
+
buildWalletHeaders,
|
|
254
|
+
maxAttempts: remainingPayments,
|
|
255
|
+
callbacks,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const newTotal = currentAttempts + outcome.attemptsUsed;
|
|
259
|
+
globalPaymentAttempts.set(walletAddress, newTotal);
|
|
260
|
+
} finally {
|
|
261
|
+
releaseLock();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw new Error('Setu: max attempts exceeded');
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create a Setu-backed AI model.
|
|
271
|
+
*
|
|
272
|
+
* Uses native AI SDK providers:
|
|
273
|
+
* - OpenAI models → /v1/responses (via @ai-sdk/openai)
|
|
274
|
+
* - Anthropic models → /v1/messages (via @ai-sdk/anthropic)
|
|
275
|
+
* - Moonshot models → /v1/chat/completions (via @ai-sdk/openai-compatible)
|
|
276
|
+
*
|
|
277
|
+
* Provider is determined by options.providerNpm from catalog.
|
|
278
|
+
*/
|
|
279
|
+
export function createSetuModel(
|
|
280
|
+
model: string,
|
|
281
|
+
auth: SetuAuth,
|
|
282
|
+
options: SetuProviderOptions = {},
|
|
283
|
+
) {
|
|
284
|
+
const baseURL = `${trimTrailingSlash(
|
|
285
|
+
options.baseURL ?? DEFAULT_BASE_URL,
|
|
286
|
+
)}/v1`;
|
|
287
|
+
const customFetch = createSetuFetch(auth, options);
|
|
288
|
+
const providerNpm = options.providerNpm ?? '@ai-sdk/openai';
|
|
289
|
+
|
|
290
|
+
if (providerNpm === '@ai-sdk/anthropic') {
|
|
291
|
+
const anthropic = createAnthropic({
|
|
292
|
+
baseURL,
|
|
293
|
+
apiKey: 'setu-wallet-auth',
|
|
294
|
+
fetch: customFetch,
|
|
295
|
+
});
|
|
296
|
+
return anthropic(model);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (providerNpm === '@ai-sdk/openai-compatible') {
|
|
300
|
+
const compatible = createOpenAICompatible({
|
|
301
|
+
name: 'setu-moonshot',
|
|
302
|
+
baseURL,
|
|
303
|
+
headers: {
|
|
304
|
+
Authorization: 'Bearer setu-wallet-auth',
|
|
305
|
+
},
|
|
306
|
+
fetch: customFetch,
|
|
307
|
+
});
|
|
308
|
+
return compatible(model);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (providerNpm === '@ai-sdk/google') {
|
|
312
|
+
const google = createGoogleGenerativeAI({
|
|
313
|
+
baseURL,
|
|
314
|
+
apiKey: 'setu-wallet-auth',
|
|
315
|
+
fetch: customFetch,
|
|
316
|
+
});
|
|
317
|
+
return google(model);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Default to OpenAI
|
|
321
|
+
const openai = createOpenAI({
|
|
322
|
+
baseURL,
|
|
323
|
+
apiKey: 'setu-wallet-auth',
|
|
324
|
+
fetch: customFetch,
|
|
325
|
+
});
|
|
326
|
+
return openai.responses(model);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function trimTrailingSlash(url: string) {
|
|
330
|
+
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function signNonce(nonce: string, secretKey: Uint8Array): string {
|
|
334
|
+
const data = new TextEncoder().encode(nonce);
|
|
335
|
+
const signature = nacl.sign.detached(data, secretKey);
|
|
336
|
+
return bs58.encode(signature);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
type PaymentRequirementResponse = {
|
|
340
|
+
accepts?: unknown;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
function pickPaymentRequirement(
|
|
344
|
+
payload: unknown,
|
|
345
|
+
): ExactPaymentRequirement | null {
|
|
346
|
+
const acceptsValue =
|
|
347
|
+
typeof payload === 'object' && payload !== null
|
|
348
|
+
? (payload as PaymentRequirementResponse).accepts
|
|
349
|
+
: undefined;
|
|
350
|
+
const accepts = Array.isArray(acceptsValue)
|
|
351
|
+
? (acceptsValue as ExactPaymentRequirement[])
|
|
352
|
+
: [];
|
|
353
|
+
return accepts.find((option) => option && option.scheme === 'exact') ?? null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function handlePayment(args: {
|
|
357
|
+
requirement: ExactPaymentRequirement;
|
|
358
|
+
keypair: Keypair;
|
|
359
|
+
rpcURL: string;
|
|
360
|
+
baseURL: string;
|
|
361
|
+
baseFetch: typeof fetch;
|
|
362
|
+
buildWalletHeaders: () => Record<string, string>;
|
|
363
|
+
maxAttempts: number;
|
|
364
|
+
callbacks: SetuPaymentCallbacks;
|
|
365
|
+
}): Promise<{ attemptsUsed: number }> {
|
|
366
|
+
let attempts = 0;
|
|
367
|
+
while (attempts < args.maxAttempts) {
|
|
368
|
+
const result = await processSinglePayment(args);
|
|
369
|
+
attempts += result.attempts;
|
|
370
|
+
const balanceValue =
|
|
371
|
+
typeof result.balance === 'number'
|
|
372
|
+
? result.balance
|
|
373
|
+
: result.balance != null
|
|
374
|
+
? Number(result.balance)
|
|
375
|
+
: undefined;
|
|
376
|
+
if (
|
|
377
|
+
balanceValue == null ||
|
|
378
|
+
Number.isNaN(balanceValue) ||
|
|
379
|
+
balanceValue >= 0
|
|
380
|
+
) {
|
|
381
|
+
return { attemptsUsed: attempts };
|
|
382
|
+
}
|
|
383
|
+
console.log(
|
|
384
|
+
`Setu balance still negative (${balanceValue.toFixed(8)}). Sending another top-up...`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
throw new Error(`Setu: payment failed after ${attempts} additional top-ups.`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function processSinglePayment(args: {
|
|
391
|
+
requirement: ExactPaymentRequirement;
|
|
392
|
+
keypair: Keypair;
|
|
393
|
+
rpcURL: string;
|
|
394
|
+
baseURL: string;
|
|
395
|
+
baseFetch: typeof fetch;
|
|
396
|
+
buildWalletHeaders: () => Record<string, string>;
|
|
397
|
+
callbacks: SetuPaymentCallbacks;
|
|
398
|
+
}): Promise<{ attempts: number; balance?: number | string }> {
|
|
399
|
+
args.callbacks.onPaymentSigning?.();
|
|
400
|
+
|
|
401
|
+
let paymentPayload: PaymentPayload;
|
|
402
|
+
try {
|
|
403
|
+
paymentPayload = await createPaymentPayload(args);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
406
|
+
const userMsg = `Payment failed: ${simplifyPaymentError(errMsg)}`;
|
|
407
|
+
args.callbacks.onPaymentError?.(userMsg);
|
|
408
|
+
throw new Error(`Setu: ${userMsg}`);
|
|
409
|
+
}
|
|
410
|
+
const walletHeaders = args.buildWalletHeaders();
|
|
411
|
+
const headers = {
|
|
412
|
+
'Content-Type': 'application/json',
|
|
413
|
+
...walletHeaders,
|
|
414
|
+
};
|
|
415
|
+
const response = await args.baseFetch(`${args.baseURL}/v1/topup`, {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
headers,
|
|
418
|
+
body: JSON.stringify({
|
|
419
|
+
paymentPayload,
|
|
420
|
+
paymentRequirement: args.requirement,
|
|
421
|
+
}),
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const rawBody = await response.text().catch(() => '');
|
|
425
|
+
if (!response.ok) {
|
|
426
|
+
if (
|
|
427
|
+
response.status === 400 &&
|
|
428
|
+
rawBody.toLowerCase().includes('already processed')
|
|
429
|
+
) {
|
|
430
|
+
console.log('Setu payment already processed; continuing.');
|
|
431
|
+
return { attempts: 1 };
|
|
432
|
+
}
|
|
433
|
+
args.callbacks.onPaymentError?.(`Topup failed: ${response.status}`);
|
|
434
|
+
throw new Error(`Setu topup failed (${response.status}): ${rawBody}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let parsed: PaymentResponse | undefined;
|
|
438
|
+
try {
|
|
439
|
+
parsed = rawBody ? (JSON.parse(rawBody) as PaymentResponse) : undefined;
|
|
440
|
+
} catch {
|
|
441
|
+
parsed = undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (parsed) {
|
|
445
|
+
const amountUsd =
|
|
446
|
+
typeof parsed.amount_usd === 'string'
|
|
447
|
+
? parseFloat(parsed.amount_usd)
|
|
448
|
+
: (parsed.amount_usd ?? parsed.amount ?? 0);
|
|
449
|
+
const newBalance =
|
|
450
|
+
typeof parsed.new_balance === 'string'
|
|
451
|
+
? parseFloat(parsed.new_balance)
|
|
452
|
+
: (parsed.new_balance ?? parsed.balance ?? 0);
|
|
453
|
+
args.callbacks.onPaymentComplete?.({
|
|
454
|
+
amountUsd,
|
|
455
|
+
newBalance,
|
|
456
|
+
transactionId: parsed.transaction,
|
|
457
|
+
});
|
|
458
|
+
console.log(
|
|
459
|
+
`Setu payment complete: +$${amountUsd} (balance: $${newBalance})`,
|
|
460
|
+
);
|
|
461
|
+
return { attempts: 1, balance: newBalance };
|
|
462
|
+
}
|
|
463
|
+
console.log('Setu payment complete.');
|
|
464
|
+
return { attempts: 1 };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function createPaymentPayload(args: {
|
|
468
|
+
requirement: ExactPaymentRequirement;
|
|
469
|
+
keypair: Keypair;
|
|
470
|
+
rpcURL: string;
|
|
471
|
+
}) {
|
|
472
|
+
const privateKeyBase58 = bs58.encode(args.keypair.secretKey);
|
|
473
|
+
const signer = await svm.createSignerFromBase58(privateKeyBase58);
|
|
474
|
+
const header = await createPaymentHeader(
|
|
475
|
+
signer,
|
|
476
|
+
1,
|
|
477
|
+
args.requirement as PaymentRequirements,
|
|
478
|
+
{
|
|
479
|
+
svmConfig: {
|
|
480
|
+
rpcUrl: args.rpcURL,
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
const decoded = JSON.parse(
|
|
485
|
+
Buffer.from(header, 'base64').toString('utf-8'),
|
|
486
|
+
) as {
|
|
487
|
+
payload: { transaction: string };
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
x402Version: 1,
|
|
492
|
+
scheme: 'exact',
|
|
493
|
+
network: args.requirement.network,
|
|
494
|
+
payload: {
|
|
495
|
+
transaction: decoded.payload.transaction,
|
|
496
|
+
},
|
|
497
|
+
} as PaymentPayload;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export type SetuBalanceResponse = {
|
|
501
|
+
walletAddress: string;
|
|
502
|
+
balance: number;
|
|
503
|
+
totalSpent: number;
|
|
504
|
+
totalTopups: number;
|
|
505
|
+
requestCount: number;
|
|
506
|
+
createdAt?: string;
|
|
507
|
+
lastRequest?: string;
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
export async function fetchSetuBalance(
|
|
511
|
+
auth: SetuAuth,
|
|
512
|
+
baseURL?: string,
|
|
513
|
+
): Promise<SetuBalanceResponse | null> {
|
|
514
|
+
try {
|
|
515
|
+
const privateKeyBytes = bs58.decode(auth.privateKey);
|
|
516
|
+
const keypair = Keypair.fromSecretKey(privateKeyBytes);
|
|
517
|
+
const walletAddress = keypair.publicKey.toBase58();
|
|
518
|
+
const url = trimTrailingSlash(baseURL ?? DEFAULT_BASE_URL);
|
|
519
|
+
|
|
520
|
+
const nonce = Date.now().toString();
|
|
521
|
+
const signature = signNonce(nonce, privateKeyBytes);
|
|
522
|
+
|
|
523
|
+
const response = await fetch(`${url}/v1/balance`, {
|
|
524
|
+
headers: {
|
|
525
|
+
'x-wallet-address': walletAddress,
|
|
526
|
+
'x-wallet-nonce': nonce,
|
|
527
|
+
'x-wallet-signature': signature,
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
if (!response.ok) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const data = (await response.json()) as {
|
|
536
|
+
wallet_address: string;
|
|
537
|
+
balance_usd: number;
|
|
538
|
+
total_spent: number;
|
|
539
|
+
total_topups: number;
|
|
540
|
+
request_count: number;
|
|
541
|
+
created_at?: string;
|
|
542
|
+
last_request?: string;
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
walletAddress: data.wallet_address,
|
|
547
|
+
balance: data.balance_usd,
|
|
548
|
+
totalSpent: data.total_spent,
|
|
549
|
+
totalTopups: data.total_topups,
|
|
550
|
+
requestCount: data.request_count,
|
|
551
|
+
createdAt: data.created_at,
|
|
552
|
+
lastRequest: data.last_request,
|
|
553
|
+
};
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function getPublicKeyFromPrivate(privateKey: string): string | null {
|
|
560
|
+
try {
|
|
561
|
+
const privateKeyBytes = bs58.decode(privateKey);
|
|
562
|
+
const keypair = Keypair.fromSecretKey(privateKeyBytes);
|
|
563
|
+
return keypair.publicKey.toBase58();
|
|
564
|
+
} catch {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const USDC_MINT_MAINNET = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
|
570
|
+
const USDC_MINT_DEVNET = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
|
|
571
|
+
|
|
572
|
+
export type SolanaUsdcBalanceResponse = {
|
|
573
|
+
walletAddress: string;
|
|
574
|
+
usdcBalance: number;
|
|
575
|
+
network: 'mainnet' | 'devnet';
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
export async function fetchSolanaUsdcBalance(
|
|
579
|
+
auth: SetuAuth,
|
|
580
|
+
network: 'mainnet' | 'devnet' = 'mainnet',
|
|
581
|
+
): Promise<SolanaUsdcBalanceResponse | null> {
|
|
582
|
+
try {
|
|
583
|
+
const privateKeyBytes = bs58.decode(auth.privateKey);
|
|
584
|
+
const keypair = Keypair.fromSecretKey(privateKeyBytes);
|
|
585
|
+
const walletAddress = keypair.publicKey.toBase58();
|
|
586
|
+
|
|
587
|
+
const rpcUrl =
|
|
588
|
+
network === 'devnet' ? 'https://api.devnet.solana.com' : DEFAULT_RPC_URL;
|
|
589
|
+
|
|
590
|
+
const usdcMint =
|
|
591
|
+
network === 'devnet' ? USDC_MINT_DEVNET : USDC_MINT_MAINNET;
|
|
592
|
+
|
|
593
|
+
const response = await fetch(rpcUrl, {
|
|
594
|
+
method: 'POST',
|
|
595
|
+
headers: { 'Content-Type': 'application/json' },
|
|
596
|
+
body: JSON.stringify({
|
|
597
|
+
jsonrpc: '2.0',
|
|
598
|
+
id: 1,
|
|
599
|
+
method: 'getTokenAccountsByOwner',
|
|
600
|
+
params: [walletAddress, { mint: usdcMint }, { encoding: 'jsonParsed' }],
|
|
601
|
+
}),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
if (!response.ok) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const data = (await response.json()) as {
|
|
609
|
+
result?: {
|
|
610
|
+
value?: Array<{
|
|
611
|
+
account: {
|
|
612
|
+
data: {
|
|
613
|
+
parsed: {
|
|
614
|
+
info: {
|
|
615
|
+
tokenAmount: {
|
|
616
|
+
uiAmount: number;
|
|
617
|
+
};
|
|
618
|
+
};
|
|
619
|
+
};
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
}>;
|
|
623
|
+
};
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const accounts = data.result?.value ?? [];
|
|
627
|
+
let totalUsdcBalance = 0;
|
|
628
|
+
|
|
629
|
+
for (const account of accounts) {
|
|
630
|
+
const uiAmount =
|
|
631
|
+
account.account.data.parsed.info.tokenAmount.uiAmount ?? 0;
|
|
632
|
+
totalUsdcBalance += uiAmount;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
walletAddress,
|
|
637
|
+
usdcBalance: totalUsdcBalance,
|
|
638
|
+
network,
|
|
639
|
+
};
|
|
640
|
+
} catch {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
}
|