@zerox1/sdk 0.2.18 → 0.2.20
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/dist/index.d.ts +240 -0
- package/dist/index.js +401 -1
- package/package.json +5 -5
- package/packages/darwin-arm64/bin/zerox1-node +0 -0
- package/packages/darwin-arm64/package.json +1 -1
- package/packages/darwin-x64/bin/zerox1-node +0 -0
- package/packages/darwin-x64/package.json +1 -1
- package/packages/linux-x64/bin/zerox1-node +0 -0
- package/packages/linux-x64/package.json +1 -1
- package/packages/win32-x64/bin/zerox1-node.exe +0 -0
- package/packages/win32-x64/package.json +1 -1
- package/src/index.ts +572 -0
package/src/index.ts
CHANGED
|
@@ -91,6 +91,306 @@ export interface SendFeedbackParams {
|
|
|
91
91
|
role: 'participant' | 'notary'
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// COUNTER negotiation types
|
|
96
|
+
//
|
|
97
|
+
// PROPOSE and COUNTER envelopes share a structured payload layout:
|
|
98
|
+
//
|
|
99
|
+
// [bytes 0-15] LE i128 — bid amount in USDC microunits (0 = unspecified)
|
|
100
|
+
// [bytes 16..] JSON — {"max_rounds": u8, "message": str} (PROPOSE)
|
|
101
|
+
// {"round": u8, "max_rounds": u8, "message": str} (COUNTER)
|
|
102
|
+
//
|
|
103
|
+
// Both sides can counter-propose up to maxRounds times (default: 2).
|
|
104
|
+
// The proposer gets maxRounds = 3 if their average reputation score >= 70.
|
|
105
|
+
// Round numbering is 1-indexed: first counter = round 1, second = round 2.
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
/** Decoded content of an incoming PROPOSE envelope payload. */
|
|
109
|
+
export interface ProposePayload {
|
|
110
|
+
/** Amount in USDC microunits (e.g. 1_000_000n = 1 USDC). 0n = unspecified. */
|
|
111
|
+
amount: bigint
|
|
112
|
+
/** Maximum counter rounds the proposer allows. Default: 2. */
|
|
113
|
+
maxRounds: number
|
|
114
|
+
/** Human-readable proposal message. */
|
|
115
|
+
message: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Decoded content of an incoming COUNTER envelope payload. */
|
|
119
|
+
export interface CounterPayload {
|
|
120
|
+
/** Counter-offered amount in USDC microunits. */
|
|
121
|
+
amount: bigint
|
|
122
|
+
/** Which counter round this is (1-indexed). */
|
|
123
|
+
round: number
|
|
124
|
+
/** Maximum rounds as originally set in the PROPOSE. */
|
|
125
|
+
maxRounds: number
|
|
126
|
+
/** Human-readable counter message. */
|
|
127
|
+
message: string
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface SendProposeParams {
|
|
131
|
+
/** Hex-encoded 32-byte agent ID of the target agent. */
|
|
132
|
+
recipient: string
|
|
133
|
+
/**
|
|
134
|
+
* 16-byte hex conversation ID. Auto-generated if omitted.
|
|
135
|
+
* The returned object includes the final conversation_id used.
|
|
136
|
+
*/
|
|
137
|
+
conversationId?: string
|
|
138
|
+
/** Bid amount in USDC microunits. Default: 0n (unspecified). */
|
|
139
|
+
amount?: bigint
|
|
140
|
+
/**
|
|
141
|
+
* Max counter rounds allowed. Default: 2.
|
|
142
|
+
* Set to 3 if your average reputation score is >= 70.
|
|
143
|
+
*/
|
|
144
|
+
maxRounds?: number
|
|
145
|
+
/** Proposal text (task description, terms, etc.). */
|
|
146
|
+
message: string
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface SendCounterParams {
|
|
150
|
+
/** Hex-encoded 32-byte agent ID of the counterparty. */
|
|
151
|
+
recipient: string
|
|
152
|
+
/** Conversation ID from the original PROPOSE. */
|
|
153
|
+
conversationId: string
|
|
154
|
+
/** Counter-offered amount in USDC microunits. */
|
|
155
|
+
amount: bigint
|
|
156
|
+
/** Counter round number (1-indexed). Must be <= maxRounds. */
|
|
157
|
+
round: number
|
|
158
|
+
/** maxRounds from the original PROPOSE. */
|
|
159
|
+
maxRounds: number
|
|
160
|
+
/** Explanation of your counter-offer. */
|
|
161
|
+
message?: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Decoded content of an incoming ACCEPT envelope payload. */
|
|
165
|
+
export interface AcceptPayload {
|
|
166
|
+
/**
|
|
167
|
+
* The amount being accepted in USDC microunits.
|
|
168
|
+
* Matches the most-recent COUNTER amount, or the original PROPOSE amount
|
|
169
|
+
* if no COUNTER was issued. Use this value for `lockPayment`.
|
|
170
|
+
*/
|
|
171
|
+
amount: bigint
|
|
172
|
+
/** Optional acceptance message. */
|
|
173
|
+
message: string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface SendAcceptParams {
|
|
177
|
+
/** Hex-encoded 32-byte agent ID of the agent whose offer you are accepting. */
|
|
178
|
+
recipient: string
|
|
179
|
+
/** Conversation ID from the original PROPOSE. */
|
|
180
|
+
conversationId: string
|
|
181
|
+
/**
|
|
182
|
+
* The agreed amount in USDC microunits — must match the most-recent COUNTER
|
|
183
|
+
* (or original PROPOSE if no COUNTER was sent). Both parties use this
|
|
184
|
+
* to call `lockPayment` with the correct amount.
|
|
185
|
+
*/
|
|
186
|
+
amount: bigint
|
|
187
|
+
/** Optional acceptance message. */
|
|
188
|
+
message?: string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface LockPaymentParams {
|
|
192
|
+
/** Hex-encoded 32-byte agent_id of the provider who will receive payment. */
|
|
193
|
+
provider: string
|
|
194
|
+
/** Hex-encoded 16-byte conversation ID from the negotiation. */
|
|
195
|
+
conversationId: string
|
|
196
|
+
/** Amount to lock in USDC microunits (must match the ACCEPT amount). */
|
|
197
|
+
amount: bigint
|
|
198
|
+
/** Notary fee in USDC microunits. Default: amount / 10n. */
|
|
199
|
+
notaryFee?: bigint
|
|
200
|
+
/** Solana slot timeout before provider can claim without approval. Default: 1000. */
|
|
201
|
+
timeoutSlots?: number
|
|
202
|
+
/** Hex-encoded 32-byte agent_id of a designated notary (optional). */
|
|
203
|
+
notary?: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ApprovePaymentParams {
|
|
207
|
+
/** Hex-encoded 32-byte agent_id of the requester (payer). */
|
|
208
|
+
requester: string
|
|
209
|
+
/** Hex-encoded 32-byte agent_id of the provider (payee). */
|
|
210
|
+
provider: string
|
|
211
|
+
/** Hex-encoded 16-byte conversation ID from the negotiation. */
|
|
212
|
+
conversationId: string
|
|
213
|
+
/** Hex-encoded 32-byte agent_id of the notary. Defaults to this agent (self-approval). */
|
|
214
|
+
notary?: string
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Token swap whitelist
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Default token mint addresses allowed in agent-to-agent swaps.
|
|
223
|
+
* Prevents agents from being tricked into swapping into fraudulent tokens.
|
|
224
|
+
*
|
|
225
|
+
* Both devnet and mainnet mints are included; the node validates against
|
|
226
|
+
* whichever network it is connected to.
|
|
227
|
+
*
|
|
228
|
+
* Override per-agent with `Zerox1Agent.setSwapWhitelist()`.
|
|
229
|
+
*/
|
|
230
|
+
export const DEFAULT_SWAP_WHITELIST: ReadonlySet<string> = new Set([
|
|
231
|
+
// SOL (wrapped)
|
|
232
|
+
'So11111111111111111111111111111111111111112',
|
|
233
|
+
// USDC — mainnet
|
|
234
|
+
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
235
|
+
// USDC — devnet
|
|
236
|
+
'4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU',
|
|
237
|
+
// USDT — mainnet
|
|
238
|
+
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
|
|
239
|
+
// JUP
|
|
240
|
+
'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN',
|
|
241
|
+
// BONK
|
|
242
|
+
'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
|
|
243
|
+
// RAY
|
|
244
|
+
'4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R',
|
|
245
|
+
// WIF
|
|
246
|
+
'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm',
|
|
247
|
+
// BAGS — mainnet
|
|
248
|
+
'Bags4uLBdNscWBnHmqBozrjSScnEqPx5qZBzLiqnRVN7',
|
|
249
|
+
])
|
|
250
|
+
|
|
251
|
+
export interface SwapParams {
|
|
252
|
+
/** Solana base58 mint address of the token to sell. */
|
|
253
|
+
inputMint: string
|
|
254
|
+
/** Solana base58 mint address of the token to buy. */
|
|
255
|
+
outputMint: string
|
|
256
|
+
/** Amount in input-token native units (e.g. lamports for SOL). */
|
|
257
|
+
amount: bigint
|
|
258
|
+
/** Max slippage in basis points. Default: 50 (0.5%). */
|
|
259
|
+
slippageBps?: number
|
|
260
|
+
/** Custom whitelist to use instead of DEFAULT_SWAP_WHITELIST. Pass an empty set to disable. */
|
|
261
|
+
whitelist?: ReadonlySet<string>
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface SwapResult {
|
|
265
|
+
/** Input amount actually consumed (native units). */
|
|
266
|
+
inAmount: bigint
|
|
267
|
+
/** Output amount received (native units). */
|
|
268
|
+
outAmount: bigint
|
|
269
|
+
/** Transaction signature. */
|
|
270
|
+
signature: string
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// PROPOSE / COUNTER payload encode + decode helpers
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
function writeBidPrefix(amount: bigint): Buffer {
|
|
278
|
+
const buf = Buffer.alloc(16)
|
|
279
|
+
buf.writeBigUInt64LE(amount & 0xFFFFFFFFFFFFFFFFn, 0)
|
|
280
|
+
buf.writeBigUInt64LE(amount >> 64n, 8)
|
|
281
|
+
return buf
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function readBidPrefix(raw: Buffer): bigint {
|
|
285
|
+
const lo = raw.readBigUInt64LE(0)
|
|
286
|
+
const hi = raw.readBigUInt64LE(8)
|
|
287
|
+
return (hi << 64n) | lo
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Encode a PROPOSE payload into the structured wire format:
|
|
292
|
+
* `[16-byte LE i128 amount][JSON {"max_rounds": N, "message": "..."}]`
|
|
293
|
+
*/
|
|
294
|
+
export function encodeProposePayload(
|
|
295
|
+
message: string,
|
|
296
|
+
amount: bigint = 0n,
|
|
297
|
+
maxRounds: number = 2,
|
|
298
|
+
): Buffer {
|
|
299
|
+
const prefix = writeBidPrefix(amount)
|
|
300
|
+
const json = Buffer.from(JSON.stringify({ max_rounds: maxRounds, message }))
|
|
301
|
+
return Buffer.concat([prefix, json])
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Encode a COUNTER payload into the structured wire format:
|
|
306
|
+
* `[16-byte LE i128 amount][JSON {"round": N, "max_rounds": M, "message": "..."}]`
|
|
307
|
+
*/
|
|
308
|
+
export function encodeCounterPayload(
|
|
309
|
+
amount: bigint,
|
|
310
|
+
round: number,
|
|
311
|
+
maxRounds: number,
|
|
312
|
+
message: string = '',
|
|
313
|
+
): Buffer {
|
|
314
|
+
const prefix = writeBidPrefix(amount)
|
|
315
|
+
const json = Buffer.from(JSON.stringify({ round, max_rounds: maxRounds, message }))
|
|
316
|
+
return Buffer.concat([prefix, json])
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Decode a PROPOSE envelope payload.
|
|
321
|
+
* Returns `null` if the payload is not in the structured format
|
|
322
|
+
* (e.g. a raw-string PROPOSE from an older agent).
|
|
323
|
+
*/
|
|
324
|
+
export function decodeProposePayload(payloadB64: string): ProposePayload | null {
|
|
325
|
+
const raw = Buffer.from(payloadB64, 'base64')
|
|
326
|
+
if (raw.length < 17 || raw[16] !== 0x7b /* '{' */) return null
|
|
327
|
+
try {
|
|
328
|
+
const body = JSON.parse(raw.slice(16).toString('utf8')) as Record<string, unknown>
|
|
329
|
+
return {
|
|
330
|
+
amount: readBidPrefix(raw),
|
|
331
|
+
maxRounds: Number(body['max_rounds'] ?? 2),
|
|
332
|
+
message: String(body['message'] ?? ''),
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Encode an ACCEPT payload.
|
|
341
|
+
* `[16-byte LE i128 amount][JSON {"message": "..."}]`
|
|
342
|
+
*
|
|
343
|
+
* Both parties must use the same `amount` — it is the agreed price that
|
|
344
|
+
* will be passed to `lockPayment` on-chain.
|
|
345
|
+
*/
|
|
346
|
+
export function encodeAcceptPayload(
|
|
347
|
+
amount: bigint,
|
|
348
|
+
message: string = '',
|
|
349
|
+
): Buffer {
|
|
350
|
+
const prefix = writeBidPrefix(amount)
|
|
351
|
+
const json = Buffer.from(JSON.stringify({ message }))
|
|
352
|
+
return Buffer.concat([prefix, json])
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Decode an ACCEPT envelope payload.
|
|
357
|
+
* Returns `null` if the payload is not in the structured format
|
|
358
|
+
* (older agents may send a plain-text ACCEPT).
|
|
359
|
+
*/
|
|
360
|
+
export function decodeAcceptPayload(payloadB64: string): AcceptPayload | null {
|
|
361
|
+
const raw = Buffer.from(payloadB64, 'base64')
|
|
362
|
+
if (raw.length < 17 || raw[16] !== 0x7b /* '{' */) return null
|
|
363
|
+
try {
|
|
364
|
+
const body = JSON.parse(raw.slice(16).toString('utf8')) as Record<string, unknown>
|
|
365
|
+
return {
|
|
366
|
+
amount: readBidPrefix(raw),
|
|
367
|
+
message: String(body['message'] ?? ''),
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Decode a COUNTER envelope payload.
|
|
376
|
+
* Returns `null` if the payload is not in the structured format.
|
|
377
|
+
*/
|
|
378
|
+
export function decodeCounterPayload(payloadB64: string): CounterPayload | null {
|
|
379
|
+
const raw = Buffer.from(payloadB64, 'base64')
|
|
380
|
+
if (raw.length < 17 || raw[16] !== 0x7b /* '{' */) return null
|
|
381
|
+
try {
|
|
382
|
+
const body = JSON.parse(raw.slice(16).toString('utf8')) as Record<string, unknown>
|
|
383
|
+
return {
|
|
384
|
+
amount: readBidPrefix(raw),
|
|
385
|
+
round: Number(body['round'] ?? 1),
|
|
386
|
+
maxRounds: Number(body['max_rounds'] ?? 2),
|
|
387
|
+
message: String(body['message'] ?? ''),
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
return null
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
94
394
|
// ============================================================================
|
|
95
395
|
// Hosting types
|
|
96
396
|
// ============================================================================
|
|
@@ -265,6 +565,7 @@ export class Zerox1Agent {
|
|
|
265
565
|
private port: number = 0
|
|
266
566
|
private nodeUrl: string = ''
|
|
267
567
|
private _reconnectDelay: number = 1000
|
|
568
|
+
private _swapWhitelist: ReadonlySet<string> = DEFAULT_SWAP_WHITELIST
|
|
268
569
|
|
|
269
570
|
private constructor() { }
|
|
270
571
|
|
|
@@ -561,6 +862,203 @@ export class Zerox1Agent {
|
|
|
561
862
|
})
|
|
562
863
|
}
|
|
563
864
|
|
|
865
|
+
/**
|
|
866
|
+
* Send a PROPOSE envelope.
|
|
867
|
+
*
|
|
868
|
+
* Calls POST /negotiate/propose — the node handles binary payload encoding.
|
|
869
|
+
* Returns the conversation ID used (auto-generated if not supplied)
|
|
870
|
+
* along with the send confirmation.
|
|
871
|
+
*/
|
|
872
|
+
async sendPropose(
|
|
873
|
+
params: SendProposeParams
|
|
874
|
+
): Promise<{ conversationId: string; confirmation: SentConfirmation }> {
|
|
875
|
+
const res = await fetch(`${this.nodeUrl}/negotiate/propose`, {
|
|
876
|
+
method: 'POST',
|
|
877
|
+
headers: { 'Content-Type': 'application/json' },
|
|
878
|
+
body: JSON.stringify({
|
|
879
|
+
recipient: params.recipient,
|
|
880
|
+
conversation_id: params.conversationId,
|
|
881
|
+
amount_usdc_micro: params.amount !== undefined ? Number(params.amount) : undefined,
|
|
882
|
+
max_rounds: params.maxRounds,
|
|
883
|
+
message: params.message,
|
|
884
|
+
}),
|
|
885
|
+
})
|
|
886
|
+
if (!res.ok) {
|
|
887
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` })) as Record<string, unknown>
|
|
888
|
+
throw new Error((err['error'] as string) ?? `HTTP ${res.status}`)
|
|
889
|
+
}
|
|
890
|
+
const json = await res.json() as Record<string, unknown>
|
|
891
|
+
return {
|
|
892
|
+
conversationId: json['conversation_id'] as string,
|
|
893
|
+
confirmation: { nonce: json['nonce'] as number, payloadHash: json['payload_hash'] as string },
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Send a COUNTER envelope.
|
|
899
|
+
*
|
|
900
|
+
* Calls POST /negotiate/counter — the node handles binary payload encoding.
|
|
901
|
+
* Protocol rules: `round` must be 1-indexed and <= `maxRounds`.
|
|
902
|
+
*/
|
|
903
|
+
async sendCounter(params: SendCounterParams): Promise<SentConfirmation> {
|
|
904
|
+
if (params.round < 1 || params.round > params.maxRounds) {
|
|
905
|
+
throw new RangeError(`round ${params.round} is out of range [1, ${params.maxRounds}]`)
|
|
906
|
+
}
|
|
907
|
+
const res = await fetch(`${this.nodeUrl}/negotiate/counter`, {
|
|
908
|
+
method: 'POST',
|
|
909
|
+
headers: { 'Content-Type': 'application/json' },
|
|
910
|
+
body: JSON.stringify({
|
|
911
|
+
recipient: params.recipient,
|
|
912
|
+
conversation_id: params.conversationId,
|
|
913
|
+
amount_usdc_micro: Number(params.amount),
|
|
914
|
+
round: params.round,
|
|
915
|
+
max_rounds: params.maxRounds,
|
|
916
|
+
message: params.message,
|
|
917
|
+
}),
|
|
918
|
+
})
|
|
919
|
+
if (!res.ok) {
|
|
920
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` })) as Record<string, unknown>
|
|
921
|
+
throw new Error((err['error'] as string) ?? `HTTP ${res.status}`)
|
|
922
|
+
}
|
|
923
|
+
const json = await res.json() as Record<string, unknown>
|
|
924
|
+
return { nonce: json['nonce'] as number, payloadHash: json['payload_hash'] as string }
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Send an ACCEPT envelope with the agreed amount.
|
|
929
|
+
*
|
|
930
|
+
* Calls POST /negotiate/accept — the node handles binary payload encoding.
|
|
931
|
+
* The `amount` must match the most-recent COUNTER (or original PROPOSE if
|
|
932
|
+
* there was no counter). Both parties use this value to call `lockPayment`.
|
|
933
|
+
*/
|
|
934
|
+
async sendAccept(params: SendAcceptParams): Promise<SentConfirmation> {
|
|
935
|
+
const res = await fetch(`${this.nodeUrl}/negotiate/accept`, {
|
|
936
|
+
method: 'POST',
|
|
937
|
+
headers: { 'Content-Type': 'application/json' },
|
|
938
|
+
body: JSON.stringify({
|
|
939
|
+
recipient: params.recipient,
|
|
940
|
+
conversation_id: params.conversationId,
|
|
941
|
+
amount_usdc_micro: Number(params.amount),
|
|
942
|
+
message: params.message,
|
|
943
|
+
}),
|
|
944
|
+
})
|
|
945
|
+
if (!res.ok) {
|
|
946
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` })) as Record<string, unknown>
|
|
947
|
+
throw new Error((err['error'] as string) ?? `HTTP ${res.status}`)
|
|
948
|
+
}
|
|
949
|
+
const json = await res.json() as Record<string, unknown>
|
|
950
|
+
return { nonce: json['nonce'] as number, payloadHash: json['payload_hash'] as string }
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Lock USDC in the escrow program on-chain.
|
|
955
|
+
*
|
|
956
|
+
* Call this after `sendAccept()` to fund the escrow account before the
|
|
957
|
+
* provider begins work. The node signs the Solana transaction using its
|
|
958
|
+
* own keypair (this agent is the requester / payer).
|
|
959
|
+
*
|
|
960
|
+
* The automatic lock triggered by `sendAccept()` (via the node loop) uses
|
|
961
|
+
* default parameters. Use this method for explicit control — e.g. a custom
|
|
962
|
+
* notary or timeout.
|
|
963
|
+
*
|
|
964
|
+
* @param params.amount — must match the amount in the ACCEPT payload exactly.
|
|
965
|
+
*/
|
|
966
|
+
async lockPayment(params: LockPaymentParams): Promise<void> {
|
|
967
|
+
const res = await fetch(`${this.nodeUrl}/escrow/lock`, {
|
|
968
|
+
method: 'POST',
|
|
969
|
+
headers: { 'Content-Type': 'application/json' },
|
|
970
|
+
body: JSON.stringify({
|
|
971
|
+
provider: params.provider,
|
|
972
|
+
conversation_id: params.conversationId,
|
|
973
|
+
amount_usdc_micro: Number(params.amount),
|
|
974
|
+
notary_fee: params.notaryFee !== undefined ? Number(params.notaryFee) : undefined,
|
|
975
|
+
timeout_slots: params.timeoutSlots,
|
|
976
|
+
notary: params.notary,
|
|
977
|
+
}),
|
|
978
|
+
})
|
|
979
|
+
if (!res.ok) {
|
|
980
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` })) as Record<string, unknown>
|
|
981
|
+
throw new Error(`lockPayment failed: ${(err['error'] as string) ?? res.status}`)
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Approve and release a locked escrow payment to the provider.
|
|
987
|
+
*
|
|
988
|
+
* Call this after verifying the provider's DELIVER output is satisfactory.
|
|
989
|
+
* The node signs as the approver (notary or requester).
|
|
990
|
+
*
|
|
991
|
+
* @param params.notary — defaults to this agent (self-approval when no separate notary).
|
|
992
|
+
*/
|
|
993
|
+
async approvePayment(params: ApprovePaymentParams): Promise<void> {
|
|
994
|
+
const res = await fetch(`${this.nodeUrl}/escrow/approve`, {
|
|
995
|
+
method: 'POST',
|
|
996
|
+
headers: { 'Content-Type': 'application/json' },
|
|
997
|
+
body: JSON.stringify({
|
|
998
|
+
requester: params.requester,
|
|
999
|
+
provider: params.provider,
|
|
1000
|
+
conversation_id: params.conversationId,
|
|
1001
|
+
notary: params.notary,
|
|
1002
|
+
}),
|
|
1003
|
+
})
|
|
1004
|
+
if (!res.ok) {
|
|
1005
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` })) as Record<string, unknown>
|
|
1006
|
+
throw new Error(`approvePayment failed: ${(err['error'] as string) ?? res.status}`)
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// ── Token swap ────────────────────────────────────────────────────────────
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Override the token whitelist for this agent instance.
|
|
1014
|
+
* Pass an empty Set to disable whitelist enforcement (not recommended).
|
|
1015
|
+
*/
|
|
1016
|
+
setSwapWhitelist(whitelist: ReadonlySet<string>): void {
|
|
1017
|
+
this._swapWhitelist = whitelist
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Execute a Jupiter token swap via the node's `/trade/swap` endpoint.
|
|
1022
|
+
*
|
|
1023
|
+
* Both `inputMint` and `outputMint` must be in the active whitelist
|
|
1024
|
+
* (DEFAULT_SWAP_WHITELIST unless overridden via `setSwapWhitelist()`).
|
|
1025
|
+
* This prevents agents from being deceived into swapping fraudulent tokens.
|
|
1026
|
+
*
|
|
1027
|
+
* @throws If either mint is not whitelisted, or the node rejects the swap.
|
|
1028
|
+
*/
|
|
1029
|
+
async swap(params: SwapParams): Promise<SwapResult> {
|
|
1030
|
+
const whitelist = params.whitelist ?? this._swapWhitelist
|
|
1031
|
+
if (whitelist.size > 0) {
|
|
1032
|
+
if (!whitelist.has(params.inputMint)) {
|
|
1033
|
+
throw new Error(`swap: inputMint ${params.inputMint} is not in the token whitelist`)
|
|
1034
|
+
}
|
|
1035
|
+
if (!whitelist.has(params.outputMint)) {
|
|
1036
|
+
throw new Error(`swap: outputMint ${params.outputMint} is not in the token whitelist`)
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const res = await fetch(`${this.nodeUrl}/trade/swap`, {
|
|
1041
|
+
method: 'POST',
|
|
1042
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1043
|
+
body: JSON.stringify({
|
|
1044
|
+
input_mint: params.inputMint,
|
|
1045
|
+
output_mint: params.outputMint,
|
|
1046
|
+
amount: params.amount.toString(),
|
|
1047
|
+
slippage_bps: params.slippageBps ?? 50,
|
|
1048
|
+
}),
|
|
1049
|
+
})
|
|
1050
|
+
if (!res.ok) {
|
|
1051
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` })) as Record<string, unknown>
|
|
1052
|
+
throw new Error(`swap failed: ${(err['error'] as string) ?? res.status}`)
|
|
1053
|
+
}
|
|
1054
|
+
const data = await res.json() as { in_amount: string; out_amount: string; signature: string }
|
|
1055
|
+
return {
|
|
1056
|
+
inAmount: BigInt(data.in_amount),
|
|
1057
|
+
outAmount: BigInt(data.out_amount),
|
|
1058
|
+
signature: data.signature,
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
564
1062
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
565
1063
|
|
|
566
1064
|
/** Generate a random 16-byte conversation ID as hex. */
|
|
@@ -573,6 +1071,7 @@ export class Zerox1Agent {
|
|
|
573
1071
|
/**
|
|
574
1072
|
* Encode a bid value (i128 LE) into the first 16 bytes of a payload,
|
|
575
1073
|
* followed by optional extra bytes (your terms).
|
|
1074
|
+
* @deprecated Use `encodeProposePayload()` or `encodeCounterPayload()` instead.
|
|
576
1075
|
*/
|
|
577
1076
|
encodeBidValue(value: bigint, rest: Buffer = Buffer.alloc(0)): Buffer {
|
|
578
1077
|
const buf = Buffer.alloc(16)
|
|
@@ -808,6 +1307,79 @@ export class HostedAgent {
|
|
|
808
1307
|
return this.send({ msgType: 'FEEDBACK', conversationId: params.conversationId, payload })
|
|
809
1308
|
}
|
|
810
1309
|
|
|
1310
|
+
/** Send a PROPOSE envelope via POST /hosted/negotiate/propose. */
|
|
1311
|
+
async sendPropose(
|
|
1312
|
+
params: SendProposeParams
|
|
1313
|
+
): Promise<{ conversationId: string }> {
|
|
1314
|
+
const res = await fetch(`${this.baseUrl}/hosted/negotiate/propose`, {
|
|
1315
|
+
method: 'POST',
|
|
1316
|
+
headers: {
|
|
1317
|
+
'Content-Type': 'application/json',
|
|
1318
|
+
'Authorization': `Bearer ${this.token}`,
|
|
1319
|
+
},
|
|
1320
|
+
body: JSON.stringify({
|
|
1321
|
+
recipient: params.recipient,
|
|
1322
|
+
conversation_id: params.conversationId,
|
|
1323
|
+
amount_usdc_micro: params.amount !== undefined ? Number(params.amount) : undefined,
|
|
1324
|
+
max_rounds: params.maxRounds,
|
|
1325
|
+
message: params.message,
|
|
1326
|
+
}),
|
|
1327
|
+
})
|
|
1328
|
+
if (!res.ok) {
|
|
1329
|
+
const body = await res.text()
|
|
1330
|
+
throw new Error(`hosted propose failed (${res.status}): ${body}`)
|
|
1331
|
+
}
|
|
1332
|
+
const json = await res.json() as Record<string, unknown>
|
|
1333
|
+
return { conversationId: json['conversation_id'] as string }
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** Send a COUNTER envelope via POST /hosted/negotiate/counter. */
|
|
1337
|
+
async sendCounter(params: SendCounterParams): Promise<void> {
|
|
1338
|
+
if (params.round < 1 || params.round > params.maxRounds) {
|
|
1339
|
+
throw new RangeError(`round ${params.round} is out of range [1, ${params.maxRounds}]`)
|
|
1340
|
+
}
|
|
1341
|
+
const res = await fetch(`${this.baseUrl}/hosted/negotiate/counter`, {
|
|
1342
|
+
method: 'POST',
|
|
1343
|
+
headers: {
|
|
1344
|
+
'Content-Type': 'application/json',
|
|
1345
|
+
'Authorization': `Bearer ${this.token}`,
|
|
1346
|
+
},
|
|
1347
|
+
body: JSON.stringify({
|
|
1348
|
+
recipient: params.recipient,
|
|
1349
|
+
conversation_id: params.conversationId,
|
|
1350
|
+
amount_usdc_micro: Number(params.amount),
|
|
1351
|
+
round: params.round,
|
|
1352
|
+
max_rounds: params.maxRounds,
|
|
1353
|
+
message: params.message,
|
|
1354
|
+
}),
|
|
1355
|
+
})
|
|
1356
|
+
if (!res.ok && res.status !== 204) {
|
|
1357
|
+
const body = await res.text()
|
|
1358
|
+
throw new Error(`hosted counter failed (${res.status}): ${body}`)
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** Send an ACCEPT envelope via POST /hosted/negotiate/accept. */
|
|
1363
|
+
async sendAccept(params: SendAcceptParams): Promise<void> {
|
|
1364
|
+
const res = await fetch(`${this.baseUrl}/hosted/negotiate/accept`, {
|
|
1365
|
+
method: 'POST',
|
|
1366
|
+
headers: {
|
|
1367
|
+
'Content-Type': 'application/json',
|
|
1368
|
+
'Authorization': `Bearer ${this.token}`,
|
|
1369
|
+
},
|
|
1370
|
+
body: JSON.stringify({
|
|
1371
|
+
recipient: params.recipient,
|
|
1372
|
+
conversation_id: params.conversationId,
|
|
1373
|
+
amount_usdc_micro: Number(params.amount),
|
|
1374
|
+
message: params.message,
|
|
1375
|
+
}),
|
|
1376
|
+
})
|
|
1377
|
+
if (!res.ok && res.status !== 204) {
|
|
1378
|
+
const body = await res.text()
|
|
1379
|
+
throw new Error(`hosted accept failed (${res.status}): ${body}`)
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
811
1383
|
/** Generate a random 16-byte conversation ID as hex. */
|
|
812
1384
|
newConversationId(): string {
|
|
813
1385
|
const bytes = new Uint8Array(16)
|