@agi-cli/sdk 0.1.99 → 0.1.100
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/package.json +5 -1
- package/src/config/src/index.ts +1 -0
- package/src/config/src/manager.ts +8 -2
- package/src/core/src/providers/resolver.ts +24 -2
- package/src/core/src/terminals/manager.ts +0 -2
- package/src/core/src/tools/builtin/terminal.ts +2 -1
- package/src/index.ts +8 -0
- package/src/prompts/src/providers.ts +5 -2
- package/src/providers/src/catalog-manual.ts +83 -0
- package/src/providers/src/catalog-merged.ts +9 -0
- package/src/providers/src/catalog.ts +1 -1
- package/src/providers/src/env.ts +1 -0
- package/src/providers/src/index.ts +9 -1
- package/src/providers/src/pricing.ts +1 -1
- package/src/providers/src/solforge-client.ts +305 -0
- package/src/providers/src/utils.ts +1 -1
- package/src/providers/src/validate.ts +1 -1
- package/src/types/src/auth.ts +6 -1
- package/src/types/src/provider.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.100",
|
|
4
4
|
"description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
|
|
5
5
|
"author": "ntishxyz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,12 +92,16 @@
|
|
|
92
92
|
"@ai-sdk/openai-compatible": "^1.0.18",
|
|
93
93
|
"@openauthjs/openauth": "^0.4.3",
|
|
94
94
|
"@openrouter/ai-sdk-provider": "^1.2.0",
|
|
95
|
+
"@solana/web3.js": "^1.95.2",
|
|
95
96
|
"ai": "^5.0.43",
|
|
97
|
+
"bs58": "^6.0.0",
|
|
96
98
|
"bun-pty": "^0.3.2",
|
|
97
99
|
"diff": "^8.0.2",
|
|
98
100
|
"fast-glob": "^3.3.2",
|
|
99
101
|
"hono": "^4.9.7",
|
|
100
102
|
"opencode-anthropic-auth": "^0.0.2",
|
|
103
|
+
"tweetnacl": "^1.0.3",
|
|
104
|
+
"x402": "^0.7.1",
|
|
101
105
|
"zod": "^4.1.8"
|
|
102
106
|
},
|
|
103
107
|
"devDependencies": {
|
package/src/config/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export async function isAuthorized(
|
|
|
31
31
|
const info = auth[provider];
|
|
32
32
|
if (info?.type === 'api' && info.key) return true;
|
|
33
33
|
if (info?.type === 'oauth' && info.refresh && info.access) return true;
|
|
34
|
+
if (info?.type === 'wallet' && info.secret) return true;
|
|
34
35
|
return false;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -42,8 +43,13 @@ export async function ensureEnv(
|
|
|
42
43
|
if (readEnvKey(provider)) return;
|
|
43
44
|
const { auth } = await read(projectRoot);
|
|
44
45
|
const stored = auth[provider];
|
|
45
|
-
const
|
|
46
|
-
|
|
46
|
+
const value =
|
|
47
|
+
stored?.type === 'api'
|
|
48
|
+
? stored.key
|
|
49
|
+
: stored?.type === 'wallet'
|
|
50
|
+
? stored.secret
|
|
51
|
+
: undefined;
|
|
52
|
+
if (value) setEnvKey(provider, value);
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
export async function writeDefaults(
|
|
@@ -3,14 +3,15 @@ import { anthropic, createAnthropic } from '@ai-sdk/anthropic';
|
|
|
3
3
|
import { google } from '@ai-sdk/google';
|
|
4
4
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
5
5
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
6
|
-
import { catalog } from '../../../providers/src/index.ts';
|
|
6
|
+
import { catalog, createSolforgeModel } from '../../../providers/src/index.ts';
|
|
7
7
|
|
|
8
8
|
export type ProviderName =
|
|
9
9
|
| 'openai'
|
|
10
10
|
| 'anthropic'
|
|
11
11
|
| 'google'
|
|
12
12
|
| 'openrouter'
|
|
13
|
-
| 'opencode'
|
|
13
|
+
| 'opencode'
|
|
14
|
+
| 'solforge';
|
|
14
15
|
|
|
15
16
|
export type ModelConfig = {
|
|
16
17
|
apiKey?: string;
|
|
@@ -110,6 +111,27 @@ export async function resolveModel(
|
|
|
110
111
|
return ocOpenAI(resolvedModelId);
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
if (provider === 'solforge') {
|
|
115
|
+
const privateKey = config.apiKey || process.env.SOLFORGE_PRIVATE_KEY || '';
|
|
116
|
+
if (!privateKey) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
'Solforge provider requires SOLFORGE_PRIVATE_KEY (base58 Solana secret).',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const baseURL = config.baseURL || process.env.SOLFORGE_BASE_URL;
|
|
122
|
+
const rpcURL = process.env.SOLFORGE_SOLANA_RPC_URL;
|
|
123
|
+
const topupAmount = process.env.SOLFORGE_TOPUP_MICRO_USDC;
|
|
124
|
+
return createSolforgeModel(
|
|
125
|
+
model,
|
|
126
|
+
{ privateKey },
|
|
127
|
+
{
|
|
128
|
+
baseURL,
|
|
129
|
+
rpcURL,
|
|
130
|
+
topupAmountMicroUsdc: topupAmount,
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
113
135
|
throw new Error(`Unsupported provider: ${provider}`);
|
|
114
136
|
}
|
|
115
137
|
|
|
@@ -20,8 +20,6 @@ export class TerminalManager {
|
|
|
20
20
|
private terminals = new Map<string, Terminal>();
|
|
21
21
|
private cleanupTimers = new Map<string, NodeJS.Timeout>();
|
|
22
22
|
|
|
23
|
-
constructor() {}
|
|
24
|
-
|
|
25
23
|
create(options: CreateTerminalOptions): Terminal {
|
|
26
24
|
if (this.terminals.size >= MAX_TERMINALS) {
|
|
27
25
|
throw new Error(`Maximum ${MAX_TERMINALS} terminals reached`);
|
|
@@ -186,7 +186,8 @@ export function buildTerminalTool(
|
|
|
186
186
|
|
|
187
187
|
const output = term.read(params.lines);
|
|
188
188
|
const normalized = output.map(normalizeTerminalLine);
|
|
189
|
-
const
|
|
189
|
+
const joined = normalized.join('\n');
|
|
190
|
+
const text = joined.split(String.fromCharCode(0)).join('');
|
|
190
191
|
|
|
191
192
|
const response: {
|
|
192
193
|
ok: true;
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,14 @@ export {
|
|
|
52
52
|
readEnvKey,
|
|
53
53
|
setEnvKey,
|
|
54
54
|
} from './providers/src/index.ts';
|
|
55
|
+
export {
|
|
56
|
+
createSolforgeFetch,
|
|
57
|
+
createSolforgeModel,
|
|
58
|
+
} from './providers/src/index.ts';
|
|
59
|
+
export type {
|
|
60
|
+
SolforgeAuth,
|
|
61
|
+
SolforgeProviderOptions,
|
|
62
|
+
} from './providers/src/index.ts';
|
|
55
63
|
|
|
56
64
|
// =======================
|
|
57
65
|
// Authentication (from internal auth module)
|
|
@@ -64,8 +64,11 @@ export async function providerBasePrompt(
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// 2) Provider-family fallback for openrouter/opencode using embedded defaults
|
|
68
|
-
if (
|
|
67
|
+
// 2) Provider-family fallback for openrouter/opencode/solforge using embedded defaults
|
|
68
|
+
if (
|
|
69
|
+
(id === 'openrouter' || id === 'opencode' || id === 'solforge') &&
|
|
70
|
+
modelId
|
|
71
|
+
) {
|
|
69
72
|
const family = inferFamilyFromModel(modelId);
|
|
70
73
|
if (family) {
|
|
71
74
|
const embedded = (
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ModelInfo,
|
|
3
|
+
ProviderCatalogEntry,
|
|
4
|
+
ProviderId,
|
|
5
|
+
} from '../../types/src/index.ts';
|
|
6
|
+
|
|
7
|
+
type CatalogMap = Partial<Record<ProviderId, ProviderCatalogEntry>>;
|
|
8
|
+
|
|
9
|
+
const SOLFORGE_ID: ProviderId = 'solforge';
|
|
10
|
+
const ACCEPTED_BINDINGS = new Set(['@ai-sdk/openai', '@ai-sdk/anthropic']);
|
|
11
|
+
|
|
12
|
+
function cloneModel(model: ModelInfo): ModelInfo {
|
|
13
|
+
return {
|
|
14
|
+
...model,
|
|
15
|
+
modalities: model.modalities
|
|
16
|
+
? {
|
|
17
|
+
input: model.modalities.input
|
|
18
|
+
? [...model.modalities.input]
|
|
19
|
+
: undefined,
|
|
20
|
+
output: model.modalities.output
|
|
21
|
+
? [...model.modalities.output]
|
|
22
|
+
: undefined,
|
|
23
|
+
}
|
|
24
|
+
: undefined,
|
|
25
|
+
cost: model.cost ? { ...model.cost } : undefined,
|
|
26
|
+
limit: model.limit ? { ...model.limit } : undefined,
|
|
27
|
+
provider: model.provider ? { ...model.provider } : undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildSolforgeEntry(base: CatalogMap): ProviderCatalogEntry | null {
|
|
32
|
+
const opencodeModels = base.opencode?.models ?? [];
|
|
33
|
+
const solforgeModels = opencodeModels
|
|
34
|
+
.filter((model) => {
|
|
35
|
+
const npm = model.provider?.npm;
|
|
36
|
+
return npm != null && ACCEPTED_BINDINGS.has(npm);
|
|
37
|
+
})
|
|
38
|
+
.map(cloneModel);
|
|
39
|
+
|
|
40
|
+
if (!solforgeModels.length) return null;
|
|
41
|
+
|
|
42
|
+
// Prefer OpenAI-family models first so defaults are stable
|
|
43
|
+
solforgeModels.sort((a, b) => {
|
|
44
|
+
const providerA = a.provider?.npm ?? '';
|
|
45
|
+
const providerB = b.provider?.npm ?? '';
|
|
46
|
+
if (providerA === providerB) {
|
|
47
|
+
return a.id.localeCompare(b.id);
|
|
48
|
+
}
|
|
49
|
+
if (providerA === '@ai-sdk/openai') return -1;
|
|
50
|
+
if (providerB === '@ai-sdk/openai') return 1;
|
|
51
|
+
return providerA.localeCompare(providerB);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const defaultModelId = 'gpt-4o-mini';
|
|
55
|
+
const defaultIdx = solforgeModels.findIndex((m) => m.id === defaultModelId);
|
|
56
|
+
if (defaultIdx > 0) {
|
|
57
|
+
const [picked] = solforgeModels.splice(defaultIdx, 1);
|
|
58
|
+
solforgeModels.unshift(picked);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
id: SOLFORGE_ID,
|
|
63
|
+
label: 'Solforge',
|
|
64
|
+
env: ['SOLFORGE_PRIVATE_KEY'],
|
|
65
|
+
api: 'https://ai.solforge.sh/v1',
|
|
66
|
+
doc: 'https://ai.solforge.sh/docs',
|
|
67
|
+
npm: '@ai-sdk/openai-compatible',
|
|
68
|
+
models: solforgeModels,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function mergeManualCatalog(
|
|
73
|
+
base: CatalogMap,
|
|
74
|
+
): Record<ProviderId, ProviderCatalogEntry> {
|
|
75
|
+
const manualEntry = buildSolforgeEntry(base);
|
|
76
|
+
const merged: Record<ProviderId, ProviderCatalogEntry> = {
|
|
77
|
+
...(base as Record<ProviderId, ProviderCatalogEntry>),
|
|
78
|
+
};
|
|
79
|
+
if (manualEntry) {
|
|
80
|
+
merged[SOLFORGE_ID] = manualEntry;
|
|
81
|
+
}
|
|
82
|
+
return merged;
|
|
83
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProviderCatalogEntry,
|
|
3
|
+
ProviderId,
|
|
4
|
+
} from '../../types/src/index.ts';
|
|
5
|
+
import { catalog as generatedCatalog } from './catalog.ts';
|
|
6
|
+
import { mergeManualCatalog } from './catalog-manual.ts';
|
|
7
|
+
|
|
8
|
+
export const catalog: Record<ProviderId, ProviderCatalogEntry> =
|
|
9
|
+
mergeManualCatalog(generatedCatalog);
|
|
@@ -4598,4 +4598,4 @@ export const catalog: Record<ProviderId, ProviderCatalogEntry> = {
|
|
|
4598
4598
|
api: 'https://opencode.ai/zen/v1',
|
|
4599
4599
|
doc: 'https://opencode.ai/docs/zen',
|
|
4600
4600
|
},
|
|
4601
|
-
} as const satisfies Record<ProviderId, ProviderCatalogEntry
|
|
4601
|
+
} as const satisfies Partial<Record<ProviderId, ProviderCatalogEntry>>;
|
package/src/providers/src/env.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { isProviderAuthorized, ensureProviderEnv } from './authorization.ts';
|
|
2
|
-
export { catalog } from './catalog.ts';
|
|
2
|
+
export { catalog } from './catalog-merged.ts';
|
|
3
3
|
export type {
|
|
4
4
|
ProviderId,
|
|
5
5
|
ModelInfo,
|
|
@@ -15,3 +15,11 @@ export {
|
|
|
15
15
|
export { validateProviderModel } from './validate.ts';
|
|
16
16
|
export { estimateModelCostUsd } from './pricing.ts';
|
|
17
17
|
export { providerEnvVar, readEnvKey, setEnvKey } from './env.ts';
|
|
18
|
+
export {
|
|
19
|
+
createSolforgeFetch,
|
|
20
|
+
createSolforgeModel,
|
|
21
|
+
} from './solforge-client.ts';
|
|
22
|
+
export type {
|
|
23
|
+
SolforgeAuth,
|
|
24
|
+
SolforgeProviderOptions,
|
|
25
|
+
} from './solforge-client.ts';
|
|
@@ -0,0 +1,305 @@
|
|
|
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 { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_BASE_URL = 'http://localhost:4000';
|
|
11
|
+
const DEFAULT_RPC_URL = 'https://api.devnet.solana.com';
|
|
12
|
+
const DEFAULT_TOPUP_AMOUNT = '100000'; // $0.10
|
|
13
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
14
|
+
const DEFAULT_MAX_PAYMENT_ATTEMPTS = 20;
|
|
15
|
+
|
|
16
|
+
export type SolforgeProviderOptions = {
|
|
17
|
+
baseURL?: string;
|
|
18
|
+
rpcURL?: string;
|
|
19
|
+
network?: string;
|
|
20
|
+
topupAmountMicroUsdc?: string;
|
|
21
|
+
maxRequestAttempts?: number;
|
|
22
|
+
maxPaymentAttempts?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type SolforgeAuth = {
|
|
26
|
+
privateKey: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type ExactPaymentRequirement = {
|
|
30
|
+
scheme: 'exact';
|
|
31
|
+
network: string;
|
|
32
|
+
maxAmountRequired: string;
|
|
33
|
+
asset: string;
|
|
34
|
+
payTo: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
resource?: string;
|
|
37
|
+
extra?: Record<string, unknown>;
|
|
38
|
+
maxTimeoutSeconds?: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type PaymentPayload = {
|
|
42
|
+
x402Version: 1;
|
|
43
|
+
scheme: 'exact';
|
|
44
|
+
network: string;
|
|
45
|
+
payload: { transaction: string };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type PaymentResponse = {
|
|
49
|
+
amount_usd: number;
|
|
50
|
+
new_balance: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function createSolforgeFetch(
|
|
54
|
+
auth: SolforgeAuth,
|
|
55
|
+
options: SolforgeProviderOptions = {},
|
|
56
|
+
): typeof fetch {
|
|
57
|
+
const privateKeyBytes = bs58.decode(auth.privateKey);
|
|
58
|
+
const keypair = Keypair.fromSecretKey(privateKeyBytes);
|
|
59
|
+
const walletAddress = keypair.publicKey.toBase58();
|
|
60
|
+
const baseURL = trimTrailingSlash(options.baseURL ?? DEFAULT_BASE_URL);
|
|
61
|
+
const rpcURL = options.rpcURL ?? DEFAULT_RPC_URL;
|
|
62
|
+
const targetTopup = options.topupAmountMicroUsdc ?? DEFAULT_TOPUP_AMOUNT;
|
|
63
|
+
const maxAttempts = options.maxRequestAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
64
|
+
const maxPaymentAttempts =
|
|
65
|
+
options.maxPaymentAttempts ?? DEFAULT_MAX_PAYMENT_ATTEMPTS;
|
|
66
|
+
|
|
67
|
+
const baseFetch = globalThis.fetch.bind(globalThis);
|
|
68
|
+
let paymentAttempts = 0;
|
|
69
|
+
|
|
70
|
+
const buildWalletHeaders = () => {
|
|
71
|
+
const nonce = Date.now().toString();
|
|
72
|
+
const signature = signNonce(nonce, privateKeyBytes);
|
|
73
|
+
return {
|
|
74
|
+
'x-wallet-address': walletAddress,
|
|
75
|
+
'x-wallet-nonce': nonce,
|
|
76
|
+
'x-wallet-signature': signature,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return async (
|
|
81
|
+
input: Parameters<typeof fetch>[0],
|
|
82
|
+
init?: Parameters<typeof fetch>[1],
|
|
83
|
+
) => {
|
|
84
|
+
let attempt = 0;
|
|
85
|
+
|
|
86
|
+
while (attempt < maxAttempts) {
|
|
87
|
+
attempt++;
|
|
88
|
+
const headers = new Headers(init?.headers);
|
|
89
|
+
const walletHeaders = buildWalletHeaders();
|
|
90
|
+
headers.set('x-wallet-address', walletHeaders['x-wallet-address']);
|
|
91
|
+
headers.set('x-wallet-nonce', walletHeaders['x-wallet-nonce']);
|
|
92
|
+
headers.set('x-wallet-signature', walletHeaders['x-wallet-signature']);
|
|
93
|
+
const response = await baseFetch(input, { ...init, headers });
|
|
94
|
+
|
|
95
|
+
if (response.status !== 402) {
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const payload = await response.json().catch(() => ({}));
|
|
100
|
+
const requirement = pickPaymentRequirement(payload, targetTopup);
|
|
101
|
+
if (!requirement) {
|
|
102
|
+
throw new Error('Solforge: unsupported payment requirement');
|
|
103
|
+
}
|
|
104
|
+
if (attempt >= maxAttempts) {
|
|
105
|
+
throw new Error('Solforge: payment failed after multiple attempts');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const remainingPayments = maxPaymentAttempts - paymentAttempts;
|
|
109
|
+
if (remainingPayments <= 0) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
'Solforge: payment failed after maximum payment attempts.',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const outcome = await handlePayment({
|
|
116
|
+
requirement,
|
|
117
|
+
keypair,
|
|
118
|
+
rpcURL,
|
|
119
|
+
baseURL,
|
|
120
|
+
baseFetch,
|
|
121
|
+
buildWalletHeaders,
|
|
122
|
+
maxAttempts: remainingPayments,
|
|
123
|
+
});
|
|
124
|
+
paymentAttempts += outcome.attemptsUsed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error('Solforge: max attempts exceeded');
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createSolforgeModel(
|
|
132
|
+
model: string,
|
|
133
|
+
auth: SolforgeAuth,
|
|
134
|
+
options: SolforgeProviderOptions = {},
|
|
135
|
+
) {
|
|
136
|
+
const baseURL = `${trimTrailingSlash(
|
|
137
|
+
options.baseURL ?? DEFAULT_BASE_URL,
|
|
138
|
+
)}/v1`;
|
|
139
|
+
const fetch = createSolforgeFetch(auth, options);
|
|
140
|
+
const provider = createOpenAICompatible({
|
|
141
|
+
name: 'solforge',
|
|
142
|
+
baseURL,
|
|
143
|
+
headers: { 'Content-Type': 'application/json' },
|
|
144
|
+
fetch,
|
|
145
|
+
});
|
|
146
|
+
return provider(model);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function trimTrailingSlash(url: string) {
|
|
150
|
+
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function signNonce(nonce: string, secretKey: Uint8Array): string {
|
|
154
|
+
const data = new TextEncoder().encode(nonce);
|
|
155
|
+
const signature = nacl.sign.detached(data, secretKey);
|
|
156
|
+
return bs58.encode(signature);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type PaymentRequirementResponse = {
|
|
160
|
+
accepts?: unknown;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
function pickPaymentRequirement(
|
|
164
|
+
payload: unknown,
|
|
165
|
+
targetAmount: string,
|
|
166
|
+
): ExactPaymentRequirement | null {
|
|
167
|
+
const acceptsValue =
|
|
168
|
+
typeof payload === 'object' && payload !== null
|
|
169
|
+
? (payload as PaymentRequirementResponse).accepts
|
|
170
|
+
: undefined;
|
|
171
|
+
const accepts = Array.isArray(acceptsValue)
|
|
172
|
+
? (acceptsValue as ExactPaymentRequirement[])
|
|
173
|
+
: [];
|
|
174
|
+
const exactMatch = accepts.find(
|
|
175
|
+
(option) =>
|
|
176
|
+
option &&
|
|
177
|
+
option.scheme === 'exact' &&
|
|
178
|
+
option.maxAmountRequired === targetAmount,
|
|
179
|
+
);
|
|
180
|
+
if (exactMatch) return exactMatch;
|
|
181
|
+
const fallback = accepts.find(
|
|
182
|
+
(option) => option && option.scheme === 'exact',
|
|
183
|
+
);
|
|
184
|
+
return fallback ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function handlePayment(args: {
|
|
188
|
+
requirement: ExactPaymentRequirement;
|
|
189
|
+
keypair: Keypair;
|
|
190
|
+
rpcURL: string;
|
|
191
|
+
baseURL: string;
|
|
192
|
+
baseFetch: typeof fetch;
|
|
193
|
+
buildWalletHeaders: () => Record<string, string>;
|
|
194
|
+
maxAttempts: number;
|
|
195
|
+
}): Promise<{ attemptsUsed: number }> {
|
|
196
|
+
let attempts = 0;
|
|
197
|
+
while (attempts < args.maxAttempts) {
|
|
198
|
+
const result = await processSinglePayment(args);
|
|
199
|
+
attempts += result.attempts;
|
|
200
|
+
const balanceValue =
|
|
201
|
+
typeof result.balance === 'number'
|
|
202
|
+
? result.balance
|
|
203
|
+
: result.balance != null
|
|
204
|
+
? Number(result.balance)
|
|
205
|
+
: undefined;
|
|
206
|
+
if (
|
|
207
|
+
balanceValue == null ||
|
|
208
|
+
Number.isNaN(balanceValue) ||
|
|
209
|
+
balanceValue >= 0
|
|
210
|
+
) {
|
|
211
|
+
return { attemptsUsed: attempts };
|
|
212
|
+
}
|
|
213
|
+
console.log(
|
|
214
|
+
`Solforge balance still negative (${balanceValue.toFixed(8)}). Sending another top-up...`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Solforge: payment failed after ${attempts} additional top-ups.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function processSinglePayment(args: {
|
|
223
|
+
requirement: ExactPaymentRequirement;
|
|
224
|
+
keypair: Keypair;
|
|
225
|
+
rpcURL: string;
|
|
226
|
+
baseURL: string;
|
|
227
|
+
baseFetch: typeof fetch;
|
|
228
|
+
buildWalletHeaders: () => Record<string, string>;
|
|
229
|
+
}): Promise<{ attempts: number; balance?: number | string }> {
|
|
230
|
+
const paymentPayload = await createPaymentPayload(args);
|
|
231
|
+
const walletHeaders = args.buildWalletHeaders();
|
|
232
|
+
const headers = {
|
|
233
|
+
'Content-Type': 'application/json',
|
|
234
|
+
...walletHeaders,
|
|
235
|
+
};
|
|
236
|
+
const response = await args.baseFetch(`${args.baseURL}/v1/topup`, {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers,
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
paymentPayload,
|
|
241
|
+
paymentRequirement: args.requirement,
|
|
242
|
+
}),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const rawBody = await response.text().catch(() => '');
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
if (
|
|
248
|
+
response.status === 400 &&
|
|
249
|
+
rawBody.toLowerCase().includes('already processed')
|
|
250
|
+
) {
|
|
251
|
+
console.log('Solforge payment already processed; continuing.');
|
|
252
|
+
return { attempts: 1 };
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`Solforge topup failed (${response.status}): ${rawBody}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let parsed: PaymentResponse | undefined;
|
|
258
|
+
try {
|
|
259
|
+
parsed = rawBody ? (JSON.parse(rawBody) as PaymentResponse) : undefined;
|
|
260
|
+
} catch {
|
|
261
|
+
parsed = undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (parsed) {
|
|
265
|
+
console.log(
|
|
266
|
+
`Solforge payment complete: +$${parsed.amount_usd ?? 0} (balance: $${parsed.new_balance ?? 0})`,
|
|
267
|
+
);
|
|
268
|
+
return { attempts: 1, balance: parsed.new_balance };
|
|
269
|
+
}
|
|
270
|
+
console.log('Solforge payment complete.');
|
|
271
|
+
return { attempts: 1 };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function createPaymentPayload(args: {
|
|
275
|
+
requirement: ExactPaymentRequirement;
|
|
276
|
+
keypair: Keypair;
|
|
277
|
+
rpcURL: string;
|
|
278
|
+
}) {
|
|
279
|
+
const privateKeyBase58 = bs58.encode(args.keypair.secretKey);
|
|
280
|
+
const signer = await svm.createSignerFromBase58(privateKeyBase58);
|
|
281
|
+
const header = await createPaymentHeader(
|
|
282
|
+
signer,
|
|
283
|
+
1,
|
|
284
|
+
args.requirement as PaymentRequirements,
|
|
285
|
+
{
|
|
286
|
+
svmConfig: {
|
|
287
|
+
rpcUrl: args.rpcURL,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
const decoded = JSON.parse(
|
|
292
|
+
Buffer.from(header, 'base64').toString('utf-8'),
|
|
293
|
+
) as {
|
|
294
|
+
payload: { transaction: string };
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
x402Version: 1,
|
|
299
|
+
scheme: 'exact',
|
|
300
|
+
network: args.requirement.network,
|
|
301
|
+
payload: {
|
|
302
|
+
transaction: decoded.payload.transaction,
|
|
303
|
+
},
|
|
304
|
+
} as PaymentPayload;
|
|
305
|
+
}
|
package/src/types/src/auth.ts
CHANGED
|
@@ -5,6 +5,11 @@ import type { ProviderId } from './provider';
|
|
|
5
5
|
*/
|
|
6
6
|
export type ApiAuth = { type: 'api'; key: string };
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Wallet/private-key authentication
|
|
10
|
+
*/
|
|
11
|
+
export type WalletAuth = { type: 'wallet'; secret: string };
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* OAuth authentication tokens
|
|
10
15
|
*/
|
|
@@ -18,7 +23,7 @@ export type OAuth = {
|
|
|
18
23
|
/**
|
|
19
24
|
* Union of all auth types
|
|
20
25
|
*/
|
|
21
|
-
export type AuthInfo = ApiAuth | OAuth;
|
|
26
|
+
export type AuthInfo = ApiAuth | OAuth | WalletAuth;
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Collection of auth credentials per provider
|